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 @@