Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions app/controllers/Controller.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -751,4 +751,113 @@ component extends="wheels.Controller" {
rethrow;
}
}

/**
* Extracts YouTube video ID from various YouTube URL formats
* Supports: https://youtube.com/watch?v=xyz, https://youtu.be/xyz, https://www.youtube.com/embed/xyz
*/
string function extractYouTubeId(required string url) {
var id = "";
// youtu.be format
if (findNoCase("youtu.be/", arguments.url)) {
id = listLast(arguments.url, "/");
// Remove any query parameters
if (findNoCase("?", id)) {
id = listFirst(id, "?");
}
}
// youtube.com/watch?v= format
else if (findNoCase("youtube.com", arguments.url) && findNoCase("v=", arguments.url)) {
var params = listLast(arguments.url, "?");
var paramList = listToArray(params, "&");
for (var param in paramList) {
if (findNoCase("v=", param)) {
id = listLast(param, "=");
break;
}
}
}
// Already embed format
else if (findNoCase("youtube.com/embed/", arguments.url)) {
id = listLast(listFirst(arguments.url, "?"), "/");
}
return trim(id);
}

/**
* Detects if a URL is embeddable and returns embed HTML
* Supports: YouTube, Twitter
*/
string function getEmbedHtml(required string url, string width="100%", string height="400") {
var embedHtml = "";
var youtubeId = "";
var vimeoId = "";
var trimmedUrl = trim(arguments.url);

// YouTube
if (findNoCase("youtube.com", trimmedUrl) || findNoCase("youtu.be", trimmedUrl)) {
youtubeId = extractYouTubeId(trimmedUrl);
if (len(youtubeId)) {
embedHtml = '<iframe width="#arguments.width#" height="#arguments.height#" src="https://www.youtube.com/embed/#youtubeId#?rel=0" frameborder="0" allowfullscreen style="max-width: 100%; margin: 1rem 0; border-radius: 0.5rem;"></iframe>';
}
}
// Twitter/X
else if (findNoCase("twitter.com", trimmedUrl) || findNoCase("x.com", trimmedUrl)) {
embedHtml = '<blockquote class="twitter-tweet" style="margin: 1rem 0;"><a href="#trimmedUrl#"></a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>';
}

return embedHtml;
}

/**
* Checks if a URL is embeddable
*/
boolean function isEmbeddableUrl(required string url) {
var embeddableDomains = ["youtube.com", "youtu.be", "twitter.com", "x.com"];
var trimmedUrl = lcase(trim(arguments.url));

for (var domain in embeddableDomains) {
if (findNoCase(domain, trimmedUrl)) {
return true;
}
}
return false;
}

/**
* Converts plain text URLs and embeddable links into HTML
* For embeddable URLs (YouTube, Vimeo, etc.), creates embed iframes
* For other URLs, creates anchor tags
*/
string function embedAndAutoLink(required string content, string class="text--primary", string target="_blank") {
var result = arguments.content;
var urlPattern = "(https?://[^\s<""'`]+)";
var matches = reMatch(urlPattern, result);

// Remove duplicates
var uniqueUrls = {};
for (var match in matches) {
var cleanUrl = trim(match);
// Skip if it's already part of an href or src
if (!findNoCase("href='#cleanUrl#", result) && !findNoCase('href="' & cleanUrl & '"', result) && !findNoCase("src='#cleanUrl#", result) && !findNoCase('src="' & cleanUrl & '"', result)) {
uniqueUrls[cleanUrl] = cleanUrl;
}
}

// Replace each unique URL
for (var link in uniqueUrls) {
if (isEmbeddableUrl(link)) {
var embedCode = getEmbedHtml(link);
if (len(embedCode)) {
result = replace(result, link, embedCode, "all");
}
} else {
// Regular link
var linkHtml = '<a href="' & link & '" class="' & arguments.class & '" target="' & arguments.target & '">' & link & '</a>';
result = replace(result, link, linkHtml, "all");
}
}

return result;
}
}
3 changes: 3 additions & 0 deletions app/controllers/web/BlogController.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,9 @@ component extends="app.Controllers.Controller" {
Throw("Blog not found");
}

// Process embeds in content
blog.content = embedAndAutoLink(blog.content);

// Set blog post data for layout meta tags (avoids DB query in view)
request.blogPostForMeta = blog;

Expand Down
2 changes: 1 addition & 1 deletion public/javascripts/createBlog.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.