⚠️ DRAFT ISSUE - STILL IN PROGRESS
Current process (context)
Blog posts are written as .mdx files. The build pipeline transforms them before Next.js compiles the app.
Source files:
src/routes/posts/*.mdx — published blog posts
src/routes/drafts/*.mdx — draft posts
src/routes/test/*.mdx — test posts (used by Cypress)
Build scripts (utils/):
utils/transformMdx.ts / utils/transformMdx.bin.ts — compiles MDX → .mjs using @mdx-js/mdx (converts markdown syntax to JSX, syntax highlighting via rehype-highlight, heading slugs via rehype-slug)
utils/extractFrontMatter.ts / utils/extractFrontMatter.bin.ts — parses YAML frontmatter from .mdx files, validates against Zod schema, writes JSON files and barrel index.js per folder
utils/frontmatterTypings.ts — Zod schema (frontMatterSchema) and inferred types (FrontMatter, FrontMatterPlusSlug, EnrichedFrontMatterPlusSlug)
utils/generateImageBarrelFiles.ts — scans src/assets/ and generates image barrel with dimensions
utils/generateRss.ts — generates public/rss.xml from frontmatter
utils/generateSitemap.ts — generates public/sitemap.xml
Generated files (all in .gitignore, recreated on each build):
src/generated/mdx/posts/*.mjs — compiled MDX as JavaScript modules
src/generated/mdx/drafts/*.mjs
src/generated/mdx/test/*.mjs
src/generated/frontmatter/posts/*.json + index.js — extracted frontmatter per post
src/generated/frontmatter/drafts/*.json + index.js
src/generated/frontmatter/test/*.json + index.js
src/generated/tags.json — maps tag names to post slugs
src/generated/images.js — image barrel with dimensions
Runtime / routing:
src/utils/blogPosts.tsx — getAllPostFrontmatter(), getFrontmatterFromSlug(), getBlogContent(), getMetadata()
src/app/posts/[slug]/page.tsx — dynamic route; calls getBlogContent() to dynamically import the .mjs from src/generated/mdx/posts/
src/app/drafts/[slug]/page.tsx — same pattern for drafts
src/app/test/[slug]/page.tsx — same pattern for test posts
src/app/posts/page.tsx — listing page with tag filtering; calls getAllPostFrontmatter()
src/components/BlogPostFrame/ — wraps rendered post content
src/components/FrontmatterBox/ — renders series box, title, date, next-in-series link
Problem
Writing blog posts in MDX lacks TypeScript tooling support:
- No IDE autocomplete for component props
- No type checking in the editor
- Frontmatter YAML has no type enforcement
Solution
Write blog posts as plain .tsx files with two exports:
- A default React component export (the post content)
- A named typed
metadata export (replacing YAML frontmatter)
Proposed post format
import { defineMetadata } from "@/utils/blog";
export const metadata = defineMetadata({
meta: {
title: "Post Title",
description: "Description.",
dateCreated: "2026-01-01",
// image: "optional_image_key",
},
// series: { name: "my_series", part: 1, description: "My Series" },
tags: ["react"],
});
export default function MyPost() {
return (
<>
<p>Post content here...</p>
</>
);
}
defineMetadata is a simple identity function providing type inference:
export function defineMetadata(m: PostMetadata): PostMetadata { return m; }
PostMetadata should be defined based on the existing FrontMatter type in utils/frontmatterTypings.ts. defineMetadata should live in a new src/utils/blog.ts (or alongside the type).
Migration of existing MDX posts
- Run
generate:all — this compiles all .mdx files to .mjs in src/generated/mdx/posts/ (markdown already converted to JSX by @mdx-js/mdx)
- Remove
src/generated/mdx/ from .gitignore and commit those files
- Convert the
frontmatter export in each .mjs to a typed metadata export matching PostMetadata
- Rename files from
.mjs to .tsx
- Delete the source
.mdx files
The existing src/app/posts/[slug]/page.tsx wildcard handler already reads from src/generated/mdx/posts/ via getBlogContent(), so routing requires no changes.
Build pipeline changes
generate:mdx step can be removed once all .mdx files are gone
extractFrontMatter.ts currently reads YAML from .mdx files to produce src/generated/frontmatter/posts/*.json. This must be adapted to instead dynamically import each .tsx file and read its metadata export, writing the same JSON format. The rest of the pipeline (tags, RSS, sitemap, getAllPostFrontmatter()) stays unchanged.
generate:mdx script and transformMdx.ts can be deleted once migration is complete
Files to change
utils/extractFrontMatter.ts — adapt to handle .tsx metadata exports
utils/frontmatterTypings.ts — add/export PostMetadata type (or create src/types/blog.ts)
src/generated/mdx/posts/*.mjs — rename to .tsx, update metadata export
.gitignore — remove src/generated/mdx/
package.json — remove generate:mdx script once done
utils/transformMdx.ts + utils/transformMdx.bin.ts — delete once done
Current process (context)
Blog posts are written as
.mdxfiles. The build pipeline transforms them before Next.js compiles the app.Source files:
src/routes/posts/*.mdx— published blog postssrc/routes/drafts/*.mdx— draft postssrc/routes/test/*.mdx— test posts (used by Cypress)Build scripts (
utils/):utils/transformMdx.ts/utils/transformMdx.bin.ts— compiles MDX →.mjsusing@mdx-js/mdx(converts markdown syntax to JSX, syntax highlighting viarehype-highlight, heading slugs viarehype-slug)utils/extractFrontMatter.ts/utils/extractFrontMatter.bin.ts— parses YAML frontmatter from.mdxfiles, validates against Zod schema, writes JSON files and barrelindex.jsper folderutils/frontmatterTypings.ts— Zod schema (frontMatterSchema) and inferred types (FrontMatter,FrontMatterPlusSlug,EnrichedFrontMatterPlusSlug)utils/generateImageBarrelFiles.ts— scanssrc/assets/and generates image barrel with dimensionsutils/generateRss.ts— generatespublic/rss.xmlfrom frontmatterutils/generateSitemap.ts— generatespublic/sitemap.xmlGenerated files (all in
.gitignore, recreated on each build):src/generated/mdx/posts/*.mjs— compiled MDX as JavaScript modulessrc/generated/mdx/drafts/*.mjssrc/generated/mdx/test/*.mjssrc/generated/frontmatter/posts/*.json+index.js— extracted frontmatter per postsrc/generated/frontmatter/drafts/*.json+index.jssrc/generated/frontmatter/test/*.json+index.jssrc/generated/tags.json— maps tag names to post slugssrc/generated/images.js— image barrel with dimensionsRuntime / routing:
src/utils/blogPosts.tsx—getAllPostFrontmatter(),getFrontmatterFromSlug(),getBlogContent(),getMetadata()src/app/posts/[slug]/page.tsx— dynamic route; callsgetBlogContent()to dynamically import the.mjsfromsrc/generated/mdx/posts/src/app/drafts/[slug]/page.tsx— same pattern for draftssrc/app/test/[slug]/page.tsx— same pattern for test postssrc/app/posts/page.tsx— listing page with tag filtering; callsgetAllPostFrontmatter()src/components/BlogPostFrame/— wraps rendered post contentsrc/components/FrontmatterBox/— renders series box, title, date, next-in-series linkProblem
Writing blog posts in MDX lacks TypeScript tooling support:
Solution
Write blog posts as plain
.tsxfiles with two exports:metadataexport (replacing YAML frontmatter)Proposed post format
defineMetadatais a simple identity function providing type inference:PostMetadatashould be defined based on the existingFrontMattertype inutils/frontmatterTypings.ts.defineMetadatashould live in a newsrc/utils/blog.ts(or alongside the type).Migration of existing MDX posts
generate:all— this compiles all.mdxfiles to.mjsinsrc/generated/mdx/posts/(markdown already converted to JSX by@mdx-js/mdx)src/generated/mdx/from.gitignoreand commit those filesfrontmatterexport in each.mjsto a typedmetadataexport matchingPostMetadata.mjsto.tsx.mdxfilesThe existing
src/app/posts/[slug]/page.tsxwildcard handler already reads fromsrc/generated/mdx/posts/viagetBlogContent(), so routing requires no changes.Build pipeline changes
generate:mdxstep can be removed once all.mdxfiles are goneextractFrontMatter.tscurrently reads YAML from.mdxfiles to producesrc/generated/frontmatter/posts/*.json. This must be adapted to instead dynamically import each.tsxfile and read itsmetadataexport, writing the same JSON format. The rest of the pipeline (tags, RSS, sitemap,getAllPostFrontmatter()) stays unchanged.generate:mdxscript andtransformMdx.tscan be deleted once migration is completeFiles to change
utils/extractFrontMatter.ts— adapt to handle.tsxmetadata exportsutils/frontmatterTypings.ts— add/exportPostMetadatatype (or createsrc/types/blog.ts)src/generated/mdx/posts/*.mjs— rename to.tsx, update metadata export.gitignore— removesrc/generated/mdx/package.json— removegenerate:mdxscript once doneutils/transformMdx.ts+utils/transformMdx.bin.ts— delete once done