diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index 4bb5b9d..d9ceecf 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -105,18 +105,11 @@ export default async function BlogPostPage({ params }) { })} - -

- {post.frontmatter.description} -

{/* Article Content */} -
-
+
+
diff --git a/app/tailwind.css b/app/tailwind.css index f0fa8ee..eefa696 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -1068,4 +1068,43 @@ text-indent: 0px; margin-bottom: 0px; } + + /* Blog post body styling with semantic spacing */ + .post-body p { + /* Scales with font size - uses logical properties for better writing mode support */ + margin-block: 1em; + } + + /* Extra blank lines from markdown -> visible gaps that scale with font size */ + .post-body .md-gap { + /* Each "extra blank line" is one em; scales with font size */ + block-size: calc(1em * var(--gap, 1)); + margin: 0; /* no extra margins around the gap */ + line-height: 1; /* prevent tall line-height from compounding */ + } + + /* Heading rhythm for better typography */ + .post-body h1 { + margin-block: 1.5em 0.6em; + } + .post-body h2 { + margin-block: 1.4em 0.6em; + } + .post-body h3 { + margin-block: 1.2em 0.5em; + } + .post-body h4 { + margin-block: 1.1em 0.5em; + } + .post-body h5 { + margin-block: 1em 0.4em; + } + .post-body h6 { + margin-block: 1em 0.4em; + } + + /* Ensure line breaks are visible */ + .post-body br { + display: block; + } } diff --git a/lib/mdx.js b/lib/mdx.js index 190cb15..3ba71fb 100644 --- a/lib/mdx.js +++ b/lib/mdx.js @@ -135,58 +135,133 @@ function generateHeadingId(text) { /** * Convert markdown to HTML with enhanced formatting + * - Preserves extra blank lines between paragraphs as visible gaps + * (each extra blank line becomes

 

) * @param {string} markdown - Raw markdown content * @returns {string} HTML content */ function markdownToHtml(markdown) { if (!markdown) return ""; + // Normalize line endings + const GAP_TOKEN = ""; + const src = String(markdown).replace(/\r\n?/g, "\n"); + + // For 3+ consecutive newlines, keep 2 for the paragraph break and + // emit a counted gap token for additional blank lines to preserve spacing. + const withGaps = src.replace(/\n{3,}/g, (m) => { + const extra = m.length - 2; + return `\n\n`; + }); + return ( - markdown + withGaps // Headers with IDs - .replace(/^### (.*$)/gim, (match, text) => { - const id = generateHeadingId(text); - return `

${text}

`; - }) - .replace(/^## (.*$)/gim, (match, text) => { - const id = generateHeadingId(text); - return `

${text}

`; - }) - .replace(/^# (.*$)/gim, (match, text) => { - const id = generateHeadingId(text); - return `

${text}

`; - }) - // Bold and italic - .replace(/\*\*(.*?)\*\*/g, "$1") - .replace(/\*(.*?)\*/g, "$1") - // Code blocks + .replace( + /^###### (.*$)/gim, + (m, t) => `
${t}
` + ) + .replace( + /^##### (.*$)/gim, + (m, t) => `
${t}
` + ) + .replace( + /^#### (.*$)/gim, + (m, t) => `

${t}

` + ) + .replace( + /^### (.*$)/gim, + (m, t) => `

${t}

` + ) + .replace( + /^## (.*$)/gim, + (m, t) => `

${t}

` + ) + .replace( + /^# (.*$)/gim, + (m, t) => `

${t}

` + ) + + // Code fences (block) and inline code .replace( /```(\w+)?\n([\s\S]*?)\n```/g, - '
$2
' + (m, lang = "", code) => + `
${code}
` ) .replace(/`([^`]+)`/g, "$1") - // Links - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') - // Lists - .replace(/^\* (.*$)/gim, "
  • $1
  • ") - .replace(/^- (.*$)/gim, "
  • $1
  • ") - .replace(/(
  • .*<\/li>)/gim, "
      $1
    ") + + // Bold and italic (strong before em to avoid overlap issues) + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1") + + // Links and images + .replace( + /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, + (m, alt, src, title = "") => + `${alt}` + ) + .replace( + /\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, + (m, text, href, title = "") => + `${text}` + ) + // Blockquotes - .replace(/^> (.*$)/gim, "

    $1

    ") - // Horizontal rules - .replace(/^---$/gm, "
    ") - .replace(/^\*\*\*$/gm, "
    ") - // Paragraphs - .replace(/\n\n/g, "

    ") - .replace(/^(?!<[h|u|li|blockquote|hr|pre])(.*$)/gim, "

    $1

    ") - // Clean up empty paragraphs and fix list wrapping - .replace(/

    <\/p>/g, "") - .replace(/

    (.*?)<\/p>/g, (match, content) => { - return content.trim() ? match : ""; + .replace(/^(>\s?.+)(\n(>\s?.+))*$/gim, (m) => { + const inner = m.replace(/^>\s?/gm, ""); + return `

    ${inner.replace( + /\n{2,}/g, + "

    " + )}

    `; }) - .replace(/<\/ul>\s*
      /g, "") // Merge consecutive ul elements - .replace(/
        \s*<\/ul>/g, "") - ); // Remove empty ul elements + + // Lists (ul/ol) + .replace(/^(\s*[-*]\s.+(?:\n\s*[-*]\s.+)*)/gim, (m) => { + const items = m + .trim() + .split(/\n/) + .map((l) => l.replace(/^\s*[-*]\s+/, "")) + .map((t) => `
      • ${t}
      • `) + .join(""); + return `
          ${items}
        `; + }) + .replace(/^(\s*\d+\.\s.+(?:\n\s*\d+\.\s.+)*)/gim, (m) => { + const items = m + .trim() + .split(/\n/) + .map((l) => l.replace(/^\s*\d+\.\s+/, "")) + .map((t) => `
      • ${t}
      • `) + .join(""); + return `
          ${items}
        `; + }) + + // Horizontal rules + .replace(/^\s*(?:-{3,}|\*{3,})\s*$/gm, "
        ") + + // Paragraphs: + // 1) Convert double newlines to paragraph boundaries + .replace(/\n\n/g, "

        ") + // 2) Convert single line breaks to
        tags within paragraphs + .replace(/(?") + // 3) Wrap remaining bare lines that are not already block-level elements. + // (Also skip our GAP_TOKEN so we can turn it into gap paragraphs later.) + .replace( + /^(?!\s*<(h[1-6]|ul|ol|li|blockquote|hr|pre|code|table|img)\b)(?!\s*<\/)(?!\s*)(.+)$/gim, + "

        $2

        " + ) + + // Clean up truly empty paragraphs but keep   gap paragraphs + .replace(/

        \s*<\/p>/g, "") + + // Turn counted GAP tokens into explicit, styleable gap elements + .replace( + //g, + (m, n) => + `

        ` + ) + ); } /**