
Blog Update: The MDX Setup Behind This Blog
Someone asked how I write and publish these posts. Here is the honest, unglamorous answer — frontmatter, next-mdx-remote, and a pile of custom components.
I was not planning to write a post about the blog itself. But someone asked me how I actually write and ship these posts, and the honest answer took more than one DM reply. So here we are.
The constraint I set for myself
I did not want a CMS. I did not want a database table for blog posts. I did not want to log into some dashboard just to fix a typo.
I wanted to open my editor, create a file, write in it, and be done.
That one constraint decided almost everything else.
Files, not a database
Every post here is a .mdx file sitting in src/data/blog/. No admin panel, no Postgres table, no CMS webhook. If I want a new post, I create a new file. If I want to edit one, I open it and edit it. Git is my version history.
Each file starts with frontmatter — title, description, image, tags, date, and an isPublished flag so I can write a post halfway, commit it, and not have it show up on the site until it is actually ready.
---
title: 'Some Post'
description: 'A short description'
image: '/blog/some-post.svg'
tags: ['dev']
date: '2026-09-07'
isPublished: true
---
The actual content starts here.On the server, I read that file, split the frontmatter from the content using gray-matter, and pass the raw content string down to a component. That is the entire "backend."
Rendering it: next-mdx-remote, not the static compiler
This is the part people usually assume is @next/mdx compiling everything at build time. It is not. I use next-mdx-remote/rsc, which parses and renders the MDX at request time inside a React Server Component.
<MDXRemote
source={content}
components={BlogComponents}
options={{
mdxOptions: {
rehypePlugins: [[rehypeHighlight, { theme: 'github-dark' }]],
},
}}
/>Why not the static compiler? Because I wanted the content to stay a plain string that I control fully — read it from disk, filter on isPublished, decide what to do with it — instead of it becoming a page at build time the moment the file exists. It is a small tradeoff in build-time optimization for a lot more control at runtime.
Every tag is mine now
The other reason I did not just reach for plain Markdown: MDX lets every HTML tag in the output become a component I control. next-mdx-remote takes a components map, and I override the tags that actually matter — img, h1 through h3, p, lists, blockquote, and pre.
That is how the code blocks on this blog get a copy button. The pre override grabs the raw text out of the children, renders it inside a styled wrapper, and drops a CodeCopyButton next to it. Nothing fancy, just:
pre: ({ children, ...props }) => {
const codeText = getTextContent(children);
return (
<div className="group relative mb-4">
<pre {...props}>{children}</pre>
<CodeCopyButton code={codeText} />
</div>
);
},Same idea for images — the default <img> from Markdown gets swapped for Next's <Image> so I get optimization and lazy loading for free, without ever writing <Image> by hand inside a post.
Syntax highlighting is @shikijs/rehype, wired in as a rehype plugin on the MDXRemote options, running github-dark as the theme. That is also the reason a code block on this blog looks the same whether I wrote three lines or thirty — it is the actual highlighter doing the work, not CSS hacks.
What I got for free just by using files
Tags and "related posts" were almost embarrassingly simple once the content lived as files with frontmatter. I read every .mdx file, pull the tags array off each one, and:
- Filtering by tag is just an array filter — no join table, no query.
- "Related posts" is scoring shared tags between the current post and every other post and taking the top three.
- Sorting by date is comparing
frontmatter.dateacross the array.
No API route, no database round trip. It runs at request time on data that is sitting right there in the repo.
What is not solved yet
I do not have full-text search across post bodies — right now filtering only works by tag, not by searching what is actually written inside a post. And publishing still means committing a file and pushing, which is fine for me, but it is a deliberate tradeoff I am aware of, not something I stumbled into.
Both of those are "later" problems. For now, the thing that actually matters — writing a post in my editor and having it show up correctly — works exactly the way I wanted it to.
The repo behind this blog is not public, so no link to poke around in — but if you are building something similar, next-mdx-remote/rsc plus a components override map will get you most of the way there.
