Skip to content

Migrate blog posts from MDX to TSX #269

@dwjohnston

Description

@dwjohnston

⚠️ 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.tsxgetAllPostFrontmatter(), 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:

  1. A default React component export (the post content)
  2. 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

  1. Run generate:all — this compiles all .mdx files to .mjs in src/generated/mdx/posts/ (markdown already converted to JSX by @mdx-js/mdx)
  2. Remove src/generated/mdx/ from .gitignore and commit those files
  3. Convert the frontmatter export in each .mjs to a typed metadata export matching PostMetadata
  4. Rename files from .mjs to .tsx
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions