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 }) {
})}
-
-
-
+
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, "")
+
+ // 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 = "") =>
+ `
`
+ )
+ .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 ``;
+ })
+ .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) =>
+ `
`
+ )
+ );
}
/**