Construire un blog MDX avec Next.js : comment ce blog a été fait
Comment j'ai construit un blog qui tourne uniquement avec des fichiers markdown, sans CMS, avec Next.js 16 et l'App Router. Structure des dossiers, config MDX et SEO, avec le vrai flux de code.
L'essentiel
Placez vos articles en fichiers .mdx sous src/content/posts et rendez-les avec @next/mdx : le blog tourne sans CMS. Le fichier devient l'URL, et un export meta remplace le frontmatter pour la liste et les métadonnées.
Sur cette page
Le blog que vous lisez en ce moment n'a ni base de données, ni interface d'administration. Chaque article est un simple fichier markdown. Voici exactement comment il a été construit.
Pourquoi une approche basée sur des fichiers
Les outils de blog ne manquent pas, mais pour un développeur solo, la plupart sont surdimensionnés.
- CMS : interface d'admin, intégrations et facturation en prime
- Intégration Notion : pratique, mais on bute sur le rendu, la vitesse et la personnalisation
- Fichiers markdown : les articles sont versionnés avec le code, c'est gratuit, et
git pushvaut publication
J'ai choisi la troisième option. C'est celle qui a le moins de friction.
Structure des dossiers
Réduite à l'essentiel, elle ressemble à ceci.
src/
├── app/
│ ├── page.tsx # 홈 - 글 목록
│ ├── posts/[slug]/page.tsx # 글 상세
│ └── sitemap.ts # /sitemap.xml
├── content/posts/*.mdx # ← 글은 여기에만 추가
└── lib/posts.ts # 목록·메타 로딩
Le nom du fichier devient l'URL. hello.mdx s'ouvre sur /posts/hello.
Configuration MDX
Pour utiliser MDX avec l'App Router, on branche @next/mdx. Next 16 utilise Turbopack par défaut, donc les plugins remark/rehype doivent être passés par leur nom en chaîne de caractères (impossible de passer une référence de fonction).
const withMDX = createMDX({
options: {
remarkPlugins: ["remark-gfm"], // 표·취소선 지원
rehypePlugins: ["rehype-slug"], // 제목 앵커
},
})
Un export meta à la place du frontmatter
D'habitude, on écrit le titre et la date dans un bloc --- (frontmatter) en haut du markdown, mais MDX peut contenir du JavaScript, ce qui permet une approche plus propre : exporter un objet, tout simplement.
export const meta = {
title: "글 제목",
date: "2026-07-06",
category: "dev",
}
## 본문 시작
La page de liste importe dynamiquement chaque fichier et ne lit que ce meta.
const mod = await import(`@/content/posts/${slug}.mdx`)
return mod.meta // 본문 렌더 없이 메타만
Ni bibliothèque de parsing, ni index séparé.
Le SEO dès le départ
Si le but du blog est d'attirer du trafic, le SEO doit être là dès le début, pas après coup. J'ai posé d'emblée les métadonnées par article, l'Open Graph, les données structurées JSON-LD, plus sitemap.xml, robots.txt et rss.xml. J'en parlerai plus en détail dans un prochain article.
En résumé
- Un article = un fichier
.mdx, le nom de fichier fait l'URL @next/mdx+ plugins en chaînes de caractères (Turbopack)export const metaà la place du frontmatter- Le SEO dès le premier commit
Avec cette structure telle quelle, votre blog peut être en ligne en 30 minutes. C'est ainsi que je continue d'empiler des histoires de produits comme La Vague du jour.
Questions fréquentes
Pourquoi des fichiers MDX plutôt qu'un CMS ?
Les articles sont versionnés avec le code, c'est gratuit, et déployer revient à publier : c'est la friction minimale pour un développeur solo. On peut aussi y insérer librement images, code et composants.
Comment construire la liste des articles sans frontmatter ?
Chaque fichier .mdx exporte un objet meta en haut, et la page de liste importe dynamiquement chaque fichier pour ne lire que ce meta. Aucune bibliothèque de parsing n'est nécessaire.
Articles similaires
- 💻 Dev
Quand un bucket a deux propriétaires, c'est le dernier apply qui gagne
J'ai ajouté une règle de lifecycle S3 et terraform apply a échoué. La cause : une structure où deux ressources possédaient chacune la configuration lifecycle du même bucket. Le lifecycle S3 ne fonctionne pas à la règle près mais au document entier, et le dernier vainqueur efface silencieusement les règles de l'autre.
- 💻 Dev
Les tests étaient verts, mais aucune notification n'était jamais partie
Le code qui poussait des jobs dans la queue échouait à chaque fois depuis le premier jour. L'erreur était avalée, et les tests restaient verts grâce au mock. L'histoire d'un deux-points qui a tué trois fonctionnalités en silence.
- 💻 Dev
Une alerte disait que la base était morte. La base n'est jamais morte
En une journée, quatre alertes CRITICAL se sont empilées. Un 504, un Prisma P2028, un 500 sur l'admin, et « service DATABASE down ». J'ai d'abord ouvert les métriques RDS : 21 heures durant, le CPU a plafonné à 19,7 %, la base allait très bien. Ce qui était lent, ce n'était pas la base, c'était un aller-retour qui traversait l'Atlantique vingt-cinq fois. Quant à l'alerte « down », c'est le health check qui se l'était infligée à lui-même.