Pourquoi j'écris mes articles à la main en MDX (et je ne le regrette pas)
Comment j'ai construit le blog de lyrad.dev avec Fumadocs et MDX, sans base de données ni CMS, et pourquoi ce choix simplifie vraiment la vie.
Comment j'ai construit le blog de lyrad.dev avec Fumadocs et MDX, sans base de données ni CMS, et pourquoi ce choix simplifie vraiment la vie.
Si tu veux créer ton propre blog, tu te retrouves vite face à cette question :
"Est-ce que je stocke mes articles dans une base de données, ou je fais autrement ?"
Sanity, Contentful, Prisma + PostgreSQL/Supabase... les options ne manquent pas. Mais j'ai fait un choix différent pour lyrad.dev. Mes articles vivent directement dans le code, sous forme de fichiers MDX. Pas de dashboard, pas de BDD, pas de webhook pour synchroniser quoi que ce soit.
Dans cet article, je vais te montrer comment j'ai construit mon blog avec Fumadocs, pourquoi j'ai volontairement évité une base de données, et ce que ça change concrètement au quotidien.
Fumadocs, c'est un framework de documentation construit au-dessus de Next.js et MDX. À la base, il est pensé pour les docs techniques, le genre de site que tu vois chez les librairies open-source. Mais il est suffisamment flexible pour servir de socle à un blog.
Concrètement, il te fournit :
C'est l'équivalent de ce que fait Next.js avec app/ pour le routing, mais appliqué à du contenu Markdown enrichi.
C'est la vraie question. Et ma réponse tient en un mot : complexité inutile.
Quand tu penses à stocker des articles dans une base de données, tu te retrouves à gérer toute une infrastructure :
Pour un blog perso de dev ? C'est trop. Je passerai plus de temps à maintenir la plomberie qu'à écrire du contenu.
Avec un CMS ou une BDD, tu introduis plusieurs points de friction :
Voici comment j'ai assemblé tout ça étape par étape pour ce blog, depuis l'installation jusqu'au rendu des pages.
On commence par installer le cœur de Fumadocs, son UI et le gestionnaire MDX. L'installation des composants de rendu passe souvent par fumadocs-ui. Dans mon package.json, voici les dépendances principales que l'on doit retrouver :
pnpm add fumadocs-core fumadocs-ui fumadocs-mdxPour aller plus loin, j'ai aussi ajouté des plugins comme fumadocs-twoslash (pour des blocs de code interactifs typés) et fumadocs-docgen.
Ensuite, il est crucial de mettre à jour les scripts pour que Fumadocs génère les données au bon moment (via la commande fumadocs-mdx) :
"scripts": {
"dev": "fumadocs-mdx && next dev",
"build": "fumadocs-mdx && next build",
"postinstall": "fumadocs-mdx"
}Au lancement, Fumadocs va lire tes fichiers et générer un dossier caché .source qui contient le typage et tes données compilées. Pour que TypeScript s'y retrouve, on ajoute cet alias :
"compilerOptions": {
"paths": {
"fumadocs-mdx:collections/*": ["./.source/*"]
}
}On enveloppe la configuration Next.js avec createMDX pour que le framework comprenne et compile correctement nos fichiers locaux :
import type { NextConfig } from "next";
import { createMDX } from "fumadocs-mdx/next";
const nextConfig: NextConfig = {
reactStrictMode: true,
// Nécessaire pour faire fonctionner des outils comme Twoslash côté serveur
serverExternalPackages: ["typescript", "twoslash", "oxc-transform"],
};
const withMDX = createMDX();
export default withMDX(nextConfig);C'est ici que l'on définit la structure de nos articles. Avec Zod, on valide le frontmatter (les métadonnées de l'article). On y configure aussi nos plugins Markdown.
import { defineConfig, defineDocs, frontmatterSchema } from "fumadocs-mdx/config";
import { z } from "zod";
// On définit nos collections d'articles
export const docs = defineDocs({
dir: "src/content/blog", // 👈 Le dossier où vivent mes articles
docs: {
schema: frontmatterSchema.extend({
date: z.string(),
tags: z.array(z.string()).default([]),
featured: z.boolean().optional().default(false),
published: z.boolean().optional().default(true),
author: z.string(),
thumbnail: z.string(),
}),
},
});
// On configure les plugins MDX
export default defineConfig({
mdxOptions: {
// providerImportSource permet d'injecter nos propres composants interactifs
providerImportSource: "@/src/components/mdx/mdx-components",
},
});Une fois la configuration prête, on initialise l'objet source. C'est lui qu'on appellera partout dans l'application pour récupérer la liste des articles ou chercher un article spécifique :
import { docs } from 'fumadocs-mdx:collections/server';
import { loader } from 'fumadocs-core/source';
export const source = loader({
baseUrl: '/blog',
source: docs.toFumadocsSource(),
});Un des gros avantages de MDX couplé à fumadocs-ui, c'est la personnalisation de l'affichage. Je mappe les balises HTML standard vers des composants React interactifs.
Par exemple, j'ajoute un zoom sur mes images et je personnalise l'affichage des blocs de code :
import defaultMdxComponents from "fumadocs-ui/mdx";
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
import { CodeBlock as FumadocsCodeBlock, Pre } from "@/src/components/codeblock";
import type { MDXComponents } from "mdx/types";
export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultMdxComponents,
...components,
img: (props) => <ImageZoom className="w-full max-w-full" {...(props as any)} />,
pre: ({ ref: _ref, ...props }) => (
<FumadocsCodeBlock className="py-0" keepBackground {...props}>
<Pre>{props.children}</Pre>
</FumadocsCodeBlock>
),
// On peut aussi injecter des composants métiers : YouTube, Accordion...
};
}Enfin, la page Next.js ! On récupère le contenu de l'article avec notre source et on le rend statiquement. Aucune base de données, tout est fait au moment du build.
import { source } from "@/src/lib/source";
import { DocsBody } from "fumadocs-ui/page";
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { TableOfContents } from "@/src/components/articles/table-of-contents";
interface PageProps {
params: Promise<{ slug: string }>;
}
// 1. Génération automatique du SEO basé sur le frontmatter
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const page = source.getPage([slug]);
if (!page) return { title: "Page Not Found" };
return {
title: page.data.title,
description: page.data.description,
keywords: page.data.tags,
openGraph: {
title: page.data.title,
description: page.data.description,
type: "article",
},
};
}
// 2. Rendu de la page
export default async function PostPage({ params }: PageProps) {
const { slug } = await params;
// Fumadocs gère la récupération de l'article depuis le slug
const page = source.getPage([slug]);
if (!page) notFound();
const MDX = page.data.body;
return (
<main>
<h1>{page.data.title}</h1>
<DocsBody>
<MDX />
</DocsBody>
</main>
);
}Avant de regarder comment je structure mes fichiers de contenu, il est bon de rappeler que Fumadocs est divisé en quatre grandes parties modulaires :

Avec toute cette configuration en place, l'architecture de mon contenu est 100% basée sur les fichiers.
Chaque fichier .mdx contient un frontmatter YAML en haut du fichier, qui doit respecter le schéma Zod défini plus tôt :
---
title: "Pourquoi j'écris mes articles à la main en MDX..."
description: "Comment j'ai construit lyrad.dev avec Fumadocs..."
date: "2025-06-01"
tags: ["Next.js", "tutoriel"]
thumbnail: "/thumbnails/articles/fumadocs-pour-un-blog.png"
author: "daryl"
---
Contenu de l'article ici...Mon workflow pour publier un nouvel article est ultra simple :
C'est tout. Aucune interface à ouvrir, aucune BDD à interroger.
Tout le contenu est généré en statique. Les pages sont des fichiers HTML pré-rendus, servis directement depuis le CDN Vercel. Zéro latence, zéro requête runtime.
Tous mes articles sont versionnés avec Git. Si je fais une erreur, git revert. Si je veux voir l'historique d'un article, git log. C'est un niveau de traçabilité que tu n'as pas avec un CMS.
Pas d'abonnement Sanity, pas de plan payant Contentful, pas d'instance Prisma ou Supabase à maintenir. Le blog tourne sur Vercel en free tier.
Parce que c'est du MDX, je peux injecter n'importe quel composant React directement dans un article :
## Voici un composant custom
<MonComposantInteractif data={mesData} />Je vais être honnête : cette approche a ses limites.
Si tu veux laisser des non-développeurs écrire du contenu, des fichiers MDX dans un repo Git, ce n'est pas la bonne solution surtout que tu dois rebuilder à chaque push.
Mais pour un blog dev que je gère seul ? Le fichier MDX dans l'IDE, c'est imbattable.
J'aurais pu connecter une BDD, brancher un CMS, écrire mes articles dans une belle interface avec un éditeur riche. J'aurais aussi passé des heures à configurer tout ça avant d'écrire mon premier mot.
À la place, j'ai un fichier .mdx, mon IDE, et git push. En quelques secondes, l'article est en ligne.
Fumadocs m'a permis de garder toute la puissance de Next.js et MDX sans complexité supplémentaire. Le blog reste simple, rapide, maintenable.
Si tu veux lancer un blog dev sans te prendre la tête avec l'infra, c'est une stack à explorer sérieusement.