Why NextWhy Next
Back to posts
💻 Dev3 min read

Building an MDX Blog from Scratch with Next.js - How This Blog Was Made

How I built a blog that runs on nothing but markdown files, no CMS, using the Next.js 16 App Router. Folder structure, MDX setup, and SEO, walked through with the actual code.

TL;DR

Keep posts as .mdx files under src/content/posts and render them with @next/mdx - no CMS needed. The file is the URL, and a meta export replaces frontmatter for listings and metadata.

On this page

The blog you're reading right now has no database and no admin panel. Every post is just a single markdown file. Here's exactly how I built it.

Why file-based

There are plenty of blogging tools out there, but most are overkill for a solo developer.

  • CMS: comes with an admin panel, integrations, and a bill
  • Notion integration: convenient, but you hit walls with rendering, speed, and customization
  • Markdown files: posts are version-controlled with the code, it's free, and git push is publishing

I picked the third. Least friction.

Folder structure

Boiled down to the essentials, it looks like this.

src/
├── app/
│   ├── page.tsx              # 홈 - 글 목록
│   ├── posts/[slug]/page.tsx # 글 상세
│   └── sitemap.ts            # /sitemap.xml
├── content/posts/*.mdx       # ← 글은 여기에만 추가
└── lib/posts.ts              # 목록·메타 로딩

The filename becomes the URL. hello.mdx opens at /posts/hello.

MDX setup

To use MDX with the App Router, you wire up @next/mdx. Next 16 uses Turbopack by default, which means remark/rehype plugins must be passed as string names (you can't pass function references).

const withMDX = createMDX({
  options: {
    remarkPlugins: ["remark-gfm"],   // 표·취소선 지원
    rehypePlugins: ["rehype-slug"],  // 제목 앵커
  },
})

meta export instead of frontmatter

You'd normally put the title and date in a --- block (frontmatter) at the top of a markdown file, but MDX can hold JavaScript, so there's a cleaner way: just export an object.

export const meta = {
  title: "글 제목",
  date: "2026-07-06",
  category: "dev",
}

## 본문 시작

The listing page dynamically imports each file and reads only this meta.

const mod = await import(`@/content/posts/${slug}.mdx`)
return mod.meta   // 본문 렌더 없이 메타만

No parsing library, no separate index.

SEO from day one

If the point of the blog is traffic, SEO belongs at the start, not the end. I baked in per-post metadata, Open Graph, JSON-LD structured data, plus sitemap.xml, robots.txt, and rss.xml from the beginning. I'll go deeper on that in the next post.

Wrapping up

  • Each post is a single .mdx file, and the filename is the URL
  • @next/mdx with string plugins (Turbopack)
  • export const meta instead of frontmatter
  • SEO from the first commit

With this same structure, you can have your own blog up in 30 minutes. I'm using it to keep building up stories about products like Daily Wave.

Frequently asked questions

Why MDX files instead of a CMS?

Posts are version-controlled alongside the code, it's free, and deploying is publishing - the least friction for a solo developer. You can also freely embed images, code, and components.

How do you build a post list without frontmatter?

Export a meta object at the top of each .mdx file, then dynamically import the file on the listing page and read just that meta. No separate parsing library needed.

Related posts