Next.jsでMDXブログをゼロから作る - このブログの作り方
CMSなしでマークダウンファイルだけで動くブログをNext.js 16 App Routerで作った過程。フォルダ構成、MDX設定、SEOまで実際のコードの流れでまとめました。
要点まとめ
記事をsrc/content/posts配下の.mdxファイルとして置き、@next/mdxでレンダーすればCMSなしでもブログは動く。ファイルがそのままURLになり、frontmatterの代わりにmeta exportで一覧とメタデータを作る。
目次
今読んでいただいているこのブログには、データベースも管理画面もありません。記事はすべてマークダウンファイル1枚です。どうやって作ったのか、そのまま公開します。
なぜファイルベースなのか
ブログツールはたくさんありますが、個人開発者にはほとんどが過剰です。
- CMS: 管理画面・連携・料金がついてきます
- Notion連携: 便利ですが、レンダリング・速度・カスタマイズで詰まります
- マークダウンファイル: 記事がコードと一緒にバージョン管理でき、無料で、
git pushがそのまま公開です
私は3番目を選びました。摩擦が一番少ないからです。
フォルダ構成
核心だけ見るとこんな形です。
src/
├── app/
│ ├── page.tsx # 홈 - 글 목록
│ ├── posts/[slug]/page.tsx # 글 상세
│ └── sitemap.ts # /sitemap.xml
├── content/posts/*.mdx # ← 글은 여기에만 추가
└── lib/posts.ts # 목록·메타 로딩
ファイル名がそのままURLになります。hello.mdx は /posts/hello で開けます。
MDX設定
App RouterでMDXを使うには @next/mdx を組み込みます。Next 16はTurbopackがデフォルトなので、remark/rehypeプラグインを文字列の名前で渡す必要があります(関数の参照は渡せません)。
const withMDX = createMDX({
options: {
remarkPlugins: ["remark-gfm"], // 표·취소선 지원
rehypePlugins: ["rehype-slug"], // 제목 앵커
},
})
frontmatterの代わりにmeta export
普通はマークダウン冒頭の --- ブロック(frontmatter)にタイトルや日付を書きますが、MDXはJavaScriptを中に持てるので、もっとすっきりした方法があります。そのままオブジェクトをexportするんです。
export const meta = {
title: "글 제목",
date: "2026-07-06",
category: "dev",
}
## 본문 시작
一覧ページでは各ファイルを動的importして、この meta だけを読みます。
const mod = await import(`@/content/posts/${slug}.mdx`)
return mod.meta // 본문 렌더 없이 메타만
パース用ライブラリも、別途のインデックスも必要ありません。
SEOは最初から
ブログの目的が流入なら、SEOは後回しではなく最初に入れるべきです。私は記事ごとのメタデータ・Open Graph・JSON-LD構造化データに加えて、sitemap.xml・robots.txt・rss.xml まで最初から敷いておきました。この話は次の記事で詳しく扱います。
まとめ
- 記事は
.mdxファイル1枚、ファイル名がURL @next/mdx+ 文字列プラグイン(Turbopack)- frontmatterの代わりに
export const meta - SEOは最初のコミットから
この構成そのままなら、皆さんのブログも30分で立ち上げられます。私はこの方式で今日の波のようなプロダクトの話を積み重ねています。
よくある質問
なぜCMSの代わりにMDXファイルを使うのですか?
記事がコードと一緒にバージョン管理でき、無料で、デプロイがそのまま公開になるため、個人開発者にとって摩擦が一番少ないからです。画像・コード・コンポーネントも自由に入れられます。
frontmatterなしでどうやって一覧を作るのですか?
各.mdxの冒頭でmetaオブジェクトをexportし、一覧ページでファイルを動的importしてそのmetaだけを読めばOKです。別途パース用ライブラリは必要ありません。
関連記事
- 💻 開発
1つのバケットに所有者が2人いると、最後にapplyした側が勝つ
S3のlifecycleルールを1つ追加したらterraform applyが失敗した。原因は、同じバケットのlifecycle設定を2つのリソースがそれぞれ所有する構造。S3のlifecycleはルール単位ではなくドキュメント単位なので、最後の勝者が相手のルールを静かに消す。
- 💻 開発
テストは緑なのに、通知は一度も送信されていなかった
キューにジョブを入れるコードが初日から毎回失敗していた。エラーは握りつぶされ、テストはモックのおかげで緑のまま。コロン一文字が3つの機能を静かに殺した話。
- 💻 開発
DBが落ちたという通知が来た。DBは一度も落ちていなかった
1日のうちにCRITICALの通知が4つたまった。504、Prisma P2028、管理画面の500、そして「DATABASEサービスダウン」。RDSの指標をまず開いたら、21時間ずっとCPU最大19.7%で無事だった。遅かったのはDBではなく、大西洋を25回渡る往復であり、「ダウン」の通知はヘルスチェックが自分で作り出したものだった。