Le blog que vous lisez n’a ni base de données ni CMS hébergé. Chaque article est un dossier du dépôt Git, avec un index.md et ses images. Pousser un commit construit le site et publie l’article. Voici comment faire la même chose avec l’App Router de Next.js.
Pourquoi du Markdown dans Git
- Gratuit et rapide. Aucun abonnement à un CMS, aucune requête en base ; chaque page est du HTML statique.
- Versionné. Chaque modification est un commit que vous pouvez relire et annuler.
- Portable. Le Markdown survit à toutes les plateformes. Changer de framework, c’est changer le moteur de rendu, pas migrer du contenu.
- Modifiable partout. L’éditeur web de GitHub, VS Code, ou un CMS basé sur Git comme Decap ou Sveltia si quelqu’un préfère un formulaire.
La contrepartie : les rédacteurs non techniques ont besoin d’un minimum de Git, ou d’un CMS basé sur Git devant.
L’arborescence
Un dossier par article garde les images à côté du texte qui les utilise — et les liens relatifs s’affichent aussi correctement dans l’aperçu GitHub :
content/blog/
my-first-post/
index.md
cover.jpg
diagram.pngUn article commence par un front matter YAML :
---
title: My first post
date: 2026-10-08
excerpt: One or two sentences for the listing and the meta description.
cover: ./cover.jpg
tags: [Next.js, Markdown]
---
The post starts here.Les dépendances
npm install gray-matter remark remark-gfm remark-html highlight.jsgray-mattersépare le front matter du corps.remark+remark-gfmanalysent le Markdown avec les extensions de GitHub (tableaux, listes de tâches).remark-htmlle transforme en HTML.highlight.jscolore les blocs de code au moment du build : les lecteurs ne téléchargent aucun outil de coloration.
Charger les articles
import fs from 'node:fs'
import path from 'node:path'
import matter from 'gray-matter'
import { remark } from 'remark'
import remarkGfm from 'remark-gfm'
import remarkHtml from 'remark-html'
const DIR = path.join(process.cwd(), 'content/blog')
export async function getAllPosts() {
const slugs = fs.readdirSync(DIR).filter((name) =>
fs.existsSync(path.join(DIR, name, 'index.md')),
)
const posts = await Promise.all(slugs.map(getPost))
return posts.sort((a, b) => b.date.localeCompare(a.date))
}
export async function getPost(slug: string) {
const file = fs.readFileSync(path.join(DIR, slug, 'index.md'), 'utf8')
const { data, content } = matter(file)
const html = String(
await remark().use(remarkGfm).use(remarkHtml, { sanitize: false }).process(content),
)
return {
slug,
title: String(data.title),
date: new Date(data.date).toISOString().slice(0, 10),
excerpt: String(data.excerpt ?? ''),
html,
}
}sanitize: false laisse passer le HTML brut des articles — pas de souci si vous êtes le seul auteur. Si d’autres personnes peuvent soumettre des articles, continuez à nettoyer.
Comme ce code ne tourne que dans des Server Components et generateStaticParams, fs ne pose pas de problème : il s’exécute au build, jamais dans le navigateur.
Les pages
L’index liste les articles ; la route de détail est statique pour chaque slug :
import { getAllPosts, getPost } from '@/lib/blog'
export const dynamicParams = false
export async function generateStaticParams() {
return (await getAllPosts()).map(({ slug }) => ({ slug }))
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
)
}La coloration syntaxique au build
Un petit plugin remark remplace chaque nœud de code par du HTML coloré :
import hljs from 'highlight.js'
import { visit } from 'unist-util-visit'
export function highlightCode() {
return (tree: any) => {
visit(tree, 'code', (node: any, index, parent: any) => {
const lang = node.lang && hljs.getLanguage(node.lang) ? node.lang : null
const html = lang
? hljs.highlight(node.value, { language: lang }).value
: hljs.highlightAuto(node.value).value
parent.children[index!] = {
type: 'html',
value: `<pre><code class="hljs language-${lang ?? 'text'}">${html}</code></pre>`,
}
})
}
}Ajoutez-le avant remarkHtml avec .use(highlightCode), et importez un thème highlight.js comme highlight.js/styles/github-dark.css.
Images et vidéo
Next.js sert les fichiers statiques depuis public/, donc les médias des articles doivent s’y retrouver. Un minuscule script lancé avant dev et build copie tout ce qui n’est pas du Markdown :
import fs from 'node:fs'
import path from 'node:path'
const SRC = 'content/blog'
const OUT = 'public/blog-media'
fs.rmSync(OUT, { recursive: true, force: true })
for (const slug of fs.readdirSync(SRC)) {
const dir = path.join(SRC, slug)
if (!fs.statSync(dir).isDirectory()) continue
for (const file of fs.readdirSync(dir)) {
if (file.endsWith('.md')) continue
fs.mkdirSync(path.join(OUT, slug), { recursive: true })
fs.copyFileSync(path.join(dir, file), path.join(OUT, slug, file))
}
}{
"scripts": {
"predev": "node scripts/copy-blog-media.mjs",
"prebuild": "node scripts/copy-blog-media.mjs"
}
}Ensuite, dans un autre plugin remark, réécrivez les URL d’images relatives (./cover.jpg) en /blog-media/<slug>/cover.jpg. Tant que vous y êtes, transformez les images qui pointent vers des fichiers .mp4 ou .webm en éléments <video controls>, et les liens YouTube en vidéos intégrées : les auteurs continuent d’écrire la syntaxe d’image Markdown habituelle.
Ajoutez public/blog-media au .gitignore : ce dossier est généré.
Publier
Connectez le dépôt à un hébergeur statique — Cloudflare Pages, Netlify ou Vercel — avec npm run build comme commande de build. Écrire un article, c’est alors :
- Créer un dossier et un
index.md. - Commiter et pousser.
- L’hébergeur construit et déploie en quelques minutes.
Les bonus SEO qui valent le coup
generateMetadataavec le titre, l’extrait, l’URL canonique et l’image de couverture de l’article comme image Open Graph.- Un
sitemap.tsqui liste chaque article. - Du JSON-LD de type
BlogPosting. - Des identifiants sur les titres, pour pouvoir créer des liens vers les sections.
Tout cela est traité dans le SEO technique pour l’App Router de Next.js.
Quand passer à autre chose
Si plusieurs personnes non techniques publient chaque jour, ont besoin d’articles programmés ou de circuits de validation, un CMS headless commence à être rentable. Pour un site personnel, un studio ou un blog produit avec quelques articles par mois, le Markdown dans Git est difficile à battre.

