From 1102bc15f1fd6dfe7263ec81214f9fc2f68854c8 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen <115699497+zainforbjs@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:19:58 +0500 Subject: [PATCH 1/2] Change blogId dataType to string --- app/models/BlogTag.cfc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/BlogTag.cfc b/app/models/BlogTag.cfc index 1fd28104..73b15e27 100644 --- a/app/models/BlogTag.cfc +++ b/app/models/BlogTag.cfc @@ -8,10 +8,10 @@ component extends="app.Models.Model" { property(name="updatedAt", column="updatedat", dataType="datetime", defaultValue = ""); property(name="deletedAt", column="deletedat", dataType="datetime", defaultValue = ""); - property(name="blogId", column="blog_id", dataType="integer"); + property(name="blogId", column="blog_id", dataType="string"); // Associations - belongsTo(name="Blog", foreignKey="blogId"); + belongsTo(name="Blog", foreignKey="blogId"); belongsTo(name="Tag", foreignKey="tagId"); } From d42fc64507c31fa35323941e1ef619f5cbb1f927 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen <115699497+zainforbjs@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:42:09 +0500 Subject: [PATCH 2/2] Add embed helpers and auto-embed links Implemented backend and client-side embed helpers to auto-detect and render embeddable links (YouTube, Twitter/X) and convert other URLs to anchors. Added CFML functions in helpers.cfm: extractYouTubeId, getEmbedHtml, isEmbeddableUrl, and embedAndAutoLink; switched blog show view to use embedAndAutoLink for post content and included the new embedHelper.js. Add a new public/js/embedHelper.js to process markdown/html and replace matching links with responsive embed iframes, and updated showBlog.js to invoke the embed processing after swaps/DOMContentLoaded and include minor behavior/formatting tweaks in the blog view. Miscellaneous whitespace/formatting cleanups in show.cfm. --- app/views/helpers.cfm | 113 +++++++++++++++++++++++++- app/views/web/BlogController/show.cfm | 49 +++++------ public/javascripts/embedHelper.js | 1 + public/javascripts/showBlog.js | 2 +- 4 files changed, 139 insertions(+), 26 deletions(-) create mode 100644 public/javascripts/embedHelper.js diff --git a/app/views/helpers.cfm b/app/views/helpers.cfm index c96fa6ee..db50b46f 100644 --- a/app/views/helpers.cfm +++ b/app/views/helpers.cfm @@ -41,4 +41,115 @@ var emailHash = lcase(hash(lcase(trim(arguments.email)), "MD5")); return "https://www.gravatar.com/avatar/" & emailHash & "?s=" & arguments.size & "&d=404"; } - \ No newline at end of file + + /** + * 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 = ''; + } + } + // Twitter/X + else if (findNoCase("twitter.com", trimmedUrl) || findNoCase("x.com", trimmedUrl)) { + embedHtml = '
'; + } + + 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 = '' & link & ''; + result = replace(result, link, linkHtml, "all"); + } + } + + return result; + } + diff --git a/app/views/web/BlogController/show.cfm b/app/views/web/BlogController/show.cfm index af56d5d1..7b588ab6 100644 --- a/app/views/web/BlogController/show.cfm +++ b/app/views/web/BlogController/show.cfm @@ -24,9 +24,9 @@ Edit - + - @@ -316,7 +316,7 @@ } } } - + // Show/hide scroll to top button if (window.scrollY > 300) { scrollToTopBtn.style.display = 'block'; @@ -334,4 +334,5 @@ }); - \ No newline at end of file + + diff --git a/public/javascripts/embedHelper.js b/public/javascripts/embedHelper.js new file mode 100644 index 00000000..4be4db0e --- /dev/null +++ b/public/javascripts/embedHelper.js @@ -0,0 +1 @@ +const embedHelper={extractYouTubeId(e){if(!e)return"";if(e.includes("youtu.be/")){let t=e.split("youtu.be/")[1];return t.includes("?")&&(t=t.split("?")[0]),t}if(e.includes("youtube.com")&&e.includes("v=")){return new URL(e).searchParams.get("v")||""}if(e.includes("youtube.com/embed/")){let t=e.split("youtube.com/embed/")[1];return t.includes("?")&&(t=t.split("?")[0]),t}return""},extractVimeoId(e){if(!e||!e.includes("vimeo.com"))return"";let t=e.split("vimeo.com/")[1];return t.includes("?")&&(t=t.split("?")[0]),t.includes("/")&&(t=t.split("/")[0]),t},isEmbeddableUrl(e){if(!e)return!1;return["youtube.com","youtu.be","twitter.com","x.com"].some((t=>e.toLowerCase().includes(t)))},getEmbedHtml(e,t="100%",r="400"){if(!e)return"";const i=e.toLowerCase();if(i.includes("youtube.com")||i.includes("youtu.be")){const t=this.extractYouTubeId(e);if(t)return`
\n\t\t\t\t\t\n\t\t\t\t
`}if(i.includes("vimeo.com")){const t=this.extractVimeoId(e);if(t)return`
\n\t\t\t\t\t\n\t\t\t\t
`}if(i.includes("codepen.io")){const t=e.match(/codepen\.io\/\w+\/pen\/(\w+)/i);if(t&&t[1])return``}if(i.includes("spotify.com")){const t=e.match(/spotify\.com\/(track|playlist|album)\/(\w+)/i);if(t&&t[1]&&t[2]){return``}}return i.includes("twitter.com")||i.includes("x.com")?`