From feba0035d8e498f0403e511993b392f44718fa2c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Nov 2025 22:21:41 +0000 Subject: [PATCH] Fix markdown rendering in article titles This change ensures that markdown syntax in titles (like *italic* and **bold**) is properly rendered as HTML when displayed on pages, while being stripped for meta tags, browser titles, and Open Graph images. Changes: - Add stripMarkdown() function to remove markdown syntax for plain text contexts - Update page templates to render markdown in displayed titles using renderInlineMarkdown() - Strip markdown from titles in meta tags, Open Graph data, and browser title - Improve semantic HTML by using

instead of
for page titles Fixes issue where asterisks and other markdown syntax appeared literally in article titles instead of being rendered as formatted text. --- src/layouts/PageLayout.astro | 4 ++- src/lib/markdown.ts | 40 ++++++++++++++++++++++++------ src/lib/opengraph.ts | 35 +++++++++++++------------- src/pages/blog/[...slug].astro | 6 ++--- src/pages/briefs/[...slug].astro | 5 ++-- src/pages/projects/[...slug].astro | 6 ++--- 6 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/layouts/PageLayout.astro b/src/layouts/PageLayout.astro index b9a67a7..bb8017c 100644 --- a/src/layouts/PageLayout.astro +++ b/src/layouts/PageLayout.astro @@ -4,6 +4,7 @@ import Header from "@components/Header.astro"; import Footer from "@components/Footer.astro"; import { SITE } from "@consts"; import type { OpenGraphData } from "@lib/opengraph"; +import { stripMarkdown } from "@lib/markdown"; type Props = { title: string; @@ -12,12 +13,13 @@ type Props = { }; const { title, description, ogData } = Astro.props; +const plainTitle = stripMarkdown(title); --- - +
diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 4daa0cb..45512df 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -1,6 +1,6 @@ export function renderInlineMarkdown(text: string): string { if (!text) return ""; - + // Helper function to escape HTML entities const escapeHtml = (str: string): string => { return str @@ -10,26 +10,52 @@ export function renderInlineMarkdown(text: string): string { .replace(/"/g, """) .replace(/'/g, "'"); }; - + // Process inline markdown patterns let html = text // Code: `text` - escape content inside backticks, then wrap in .replace(/`([^`]+)`/g, (_, content) => `${escapeHtml(content)}`) - + // Bold: **text** or __text__ - escape content, then wrap in .replace(/\*\*([^*]+)\*\*/g, (_, content) => `${escapeHtml(content)}`) .replace(/__([^_]+)__/g, (_, content) => `${escapeHtml(content)}`) - + // Italic: *text* or _text_ (but not part of bold) - escape content, then wrap in .replace(/(? `${escapeHtml(content)}`) .replace(/(? `${escapeHtml(content)}`) - + // Strikethrough: ~~text~~ - escape content, then wrap in .replace(/~~([^~]+)~~/g, (_, content) => `${escapeHtml(content)}`); - + // Escape any remaining unprocessed text (text outside of markdown patterns) // This is tricky because we need to avoid escaping the HTML we just created // For now, we'll leave plain text unescaped since Astro should handle it - + return html; +} + +/** + * Strip markdown and HTML from text, returning plain text. + * Useful for meta tags, alt text, and other contexts where plain text is needed. + */ +export function stripMarkdown(text: string): string { + if (!text) return ""; + + return text + // Remove code: `text` + .replace(/`([^`]+)`/g, "$1") + + // Remove bold: **text** or __text__ + .replace(/\*\*([^*]+)\*\*/g, "$1") + .replace(/__([^_]+)__/g, "$1") + + // Remove italic: *text* or _text_ + .replace(/(?]*>/g, ""); } \ No newline at end of file diff --git a/src/lib/opengraph.ts b/src/lib/opengraph.ts index 2364438..c0acaf8 100644 --- a/src/lib/opengraph.ts +++ b/src/lib/opengraph.ts @@ -1,5 +1,6 @@ import type { CollectionEntry } from "astro:content"; import { SITE } from "@consts"; +import { stripMarkdown } from "./markdown"; export interface OpenGraphData { title: string; @@ -68,17 +69,17 @@ export function getPostOGData( url: string, siteUrl: string ): OpenGraphData { - const ogTitle = post.data.ogTitle || post.data.title; + const ogTitle = stripMarkdown(post.data.ogTitle || post.data.title); const ogDescription = post.data.ogDescription || post.data.description; - + let ogImage = post.data.ogImage; if (!ogImage && !post.data.noOgImage) { ogImage = generateTailgraphURL({ - title: post.data.cardTitle || post.data.title, - subtitle: post.data.date.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric" + title: stripMarkdown(post.data.cardTitle || post.data.title), + subtitle: post.data.date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric" }), author: "plx", theme: "dark", @@ -86,7 +87,7 @@ export function getPostOGData( logo: `${siteUrl}/default-og-image.jpg` }); } - + return { title: ogTitle, description: ogDescription, @@ -116,21 +117,21 @@ export function getBriefOGData( url: string, siteUrl: string ): OpenGraphData { - const ogTitle = brief.data.ogTitle || brief.data.title; + const ogTitle = stripMarkdown(brief.data.ogTitle || brief.data.title); const ogDescription = brief.data.ogDescription || brief.data.description; - + let ogImage = brief.data.ogImage; if (!ogImage && !brief.data.noOgImage) { ogImage = generateTailgraphURL({ - title: brief.data.cardTitle || brief.data.title, - subtitle: category?.titlePrefix || category?.displayName || "Brief", + title: stripMarkdown(brief.data.cardTitle || brief.data.title), + subtitle: stripMarkdown(category?.titlePrefix || category?.displayName || "Brief"), author: "plx", theme: "dark", backgroundImage: "gradient", logo: `${siteUrl}/default-og-image.jpg` }); } - + return { title: ogTitle, description: ogDescription, @@ -159,13 +160,13 @@ export function getProjectOGData( url: string, siteUrl: string ): OpenGraphData { - const ogTitle = project.data.ogTitle || project.data.title; + const ogTitle = stripMarkdown(project.data.ogTitle || project.data.title); const ogDescription = project.data.ogDescription || project.data.description; - + let ogImage = project.data.ogImage; if (!ogImage && !project.data.noOgImage) { ogImage = generateTailgraphURL({ - title: project.data.title, + title: stripMarkdown(project.data.title), subtitle: "Project", author: "plx", theme: "dark", @@ -173,7 +174,7 @@ export function getProjectOGData( logo: `${siteUrl}/default-og-image.jpg` }); } - + return { title: ogTitle, description: ogDescription, diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index 38d5e1c..e048cdd 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -6,6 +6,7 @@ import FormattedDate from "@components/FormattedDate.astro"; import { readingTime } from "@lib/utils"; import BackToPrev from "@components/BackToPrev.astro"; import { getPostOGData } from "@lib/opengraph"; +import { renderInlineMarkdown } from "@lib/markdown"; export async function getStaticPaths() { const posts = (await getCollection("blog")) @@ -22,6 +23,7 @@ const post = Astro.props; const { Content } = await post.render(); const ogData = getPostOGData(post, Astro.url.toString(), Astro.site?.toString() || ""); +const renderedTitle = renderInlineMarkdown(post.data.title); --- @@ -41,9 +43,7 @@ const ogData = getPostOGData(post, Astro.url.toString(), Astro.site?.toString() {readingTime(post.body)}
-
- {post.data.title} -
+

diff --git a/src/pages/briefs/[...slug].astro b/src/pages/briefs/[...slug].astro index d17d159..9db35ac 100644 --- a/src/pages/briefs/[...slug].astro +++ b/src/pages/briefs/[...slug].astro @@ -27,6 +27,7 @@ const categorySlug = extractCategoryFromSlug(brief.slug); const category = categorySlug ? getCategory(categorySlug, `src/content/briefs/${categorySlug}`) : null; const ogData = getBriefOGData(brief, category, Astro.url.toString(), Astro.site?.toString() || ""); const renderedTitlePrefix = category?.titlePrefix ? renderInlineMarkdown(category.titlePrefix) : null; +const renderedTitle = renderInlineMarkdown(brief.data.title); --- @@ -49,9 +50,7 @@ const renderedTitlePrefix = category?.titlePrefix ? renderInlineMarkdown(categor )} -
- {brief.data.title} -
+

diff --git a/src/pages/projects/[...slug].astro b/src/pages/projects/[...slug].astro index 295c984..2e74f7c 100644 --- a/src/pages/projects/[...slug].astro +++ b/src/pages/projects/[...slug].astro @@ -7,6 +7,7 @@ import { readingTime } from "@lib/utils"; import BackToPrev from "@components/BackToPrev.astro"; import Link from "@components/Link.astro"; import { getProjectOGData } from "@lib/opengraph"; +import { renderInlineMarkdown } from "@lib/markdown"; export async function getStaticPaths() { const projects = (await getCollection("projects")) @@ -23,6 +24,7 @@ const project = Astro.props; const { Content } = await project.render(); const ogData = getProjectOGData(project, Astro.url.toString(), Astro.site?.toString() || ""); +const renderedTitle = renderInlineMarkdown(project.data.title); --- @@ -42,9 +44,7 @@ const ogData = getProjectOGData(project, Astro.url.toString(), Astro.site?.toStr {readingTime(project.body)} -
- {project.data.title} -
+

{(project.data.demoURL || project.data.repoURL) && (