me
Back to all articles
Post in FrenchFrench flag
Daryl Ngako

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.

MdxNext.jsTutoriel
Pourquoi j'écris mes articles à la main en MDX (et je ne le regrette pas)

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.

C'est quoi Fumadocs ?

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 :

  • Un système de routing basé sur les fichiers : chaque fichier .mdx devient une route automatiquement
  • Un moteur de rendu MDX performant avec support des composants React
  • Une navigation, une table des matières et une recherche intégrées
  • Une configuration simple via source.config.ts

C'est l'équivalent de ce que fait Next.js avec app/ pour le routing, mais appliqué à du contenu Markdown enrichi.

Pourquoi pas de base de données ?

C'est la vraie question. Et ma réponse tient en un mot : complexité inutile.

Le piège du "stack complet pour un blog"

Quand tu penses à stocker des articles dans une base de données, tu te retrouves à gérer toute une infrastructure :

  • Un schéma avec une table posts, des colonnes title, slug, content, published_at...
  • Une interface d'administration pour créer et éditer les articles
  • Une API ou un ORM pour interroger cette BDD depuis le frontend
  • Une gestion du cache pour ne pas refaire des requêtes à chaque rendu
  • Et éventuellement un CMS headless si tu veux une UI propre

Pour un blog perso de dev ? C'est trop. Je passerai plus de temps à maintenir la plomberie qu'à écrire du contenu.

Ce que j'ai voulu éviter

Avec un CMS ou une BDD, tu introduis plusieurs points de friction :

  • Dépendance externe : si Sanity ou Contentful change son pricing ou son API, ton blog est impacté
  • Latence réseau : chaque article nécessite une requête vers un service tiers au build ou au runtime
  • Complexité du déploiement : variables d'environnement, tokens API, migrations...

Comment ça marche concrètement

Voici comment j'ai assemblé tout ça étape par étape pour ce blog, depuis l'installation jusqu'au rendu des pages.

1. Installation et dépendances (package.json)

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-mdx

Pour 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) :

package.json
"scripts": {
  "dev": "fumadocs-mdx && next dev",
  "build": "fumadocs-mdx && next build",
  "postinstall": "fumadocs-mdx"
}

2. Configuration TypeScript (tsconfig.json)

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 :

tsconfig.json
"compilerOptions": {
  "paths": {
    "fumadocs-mdx:collections/*": ["./.source/*"]
  }
}

3. Intégration Next.js (next.config.ts)

On enveloppe la configuration Next.js avec createMDX pour que le framework comprenne et compile correctement nos fichiers locaux :

next.config.ts
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);

4. Configuration de la source (source.config.ts)

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.

source.config.ts
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", 
  },
});

5. Création du loader (src/lib/source.ts)

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 :

src/lib/source.ts
import { docs } from 'fumadocs-mdx:collections/server';
import { loader } from 'fumadocs-core/source';

export const source = loader({
  baseUrl: '/blog',
  source: docs.toFumadocsSource(),
});

6. Rendu des composants MDX (src/components/mdx/mdx-components.tsx)

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 :

src/components/mdx/mdx-components.tsx
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...
  };
}

7. Génération de la page d'article (app/blog/posts/[slug]/page.tsx)

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.

app/blog/posts/[slug]/page.tsx
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>
  );
}

L'écosystème Fumadocs

Avant de regarder comment je structure mes fichiers de contenu, il est bon de rappeler que Fumadocs est divisé en quatre grandes parties modulaires :

  • Fumadocs Core : Gère la majorité de la logique sous le capot (recherche, adaptateurs de contenu, extensions Markdown).
  • Fumadocs UI : Le thème par défaut qui offre un design soigné pour les sites de documentation et des composants interactifs.
  • Content Source : La source de ton contenu. Ça peut être un CMS, mais ici on utilise Fumadocs MDX (la source officielle pour gérer des couches de données locales).
  • Fumadocs CLI : Un outil en ligne de commande pour installer des composants UI et automatiser des tâches (très utile pour customiser les layouts).

fumadocs-parts

La structure des fichiers

Avec toute cette configuration en place, l'architecture de mon contenu est 100% basée sur les fichiers.

mon-premier-article.mdx
guide-n8n-automatisation.mdx
pourquoi-jecris-mes-articles-a-la-main-en-mdx-et-je-ne-le-regrette-pas.mdx

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...

Écrire un article

Mon workflow pour publier un nouvel article est ultra simple :

  1. Je crée un fichier .mdx dans src/content/blog/
  2. J'écris mon article dans mon IDE avec l'autocomplétion (grâce au frontmatter typé)
  3. Je fais un git push
  4. Vercel détecte le commit, rebuild et redéploie

C'est tout. Aucune interface à ouvrir, aucune BDD à interroger.

Les vrais avantages

Performance maximale

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.

Le blog vit dans le code

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.

Zéro coût supplémentaire

Pas d'abonnement Sanity, pas de plan payant Contentful, pas d'instance Prisma ou Supabase à maintenir. Le blog tourne sur Vercel en free tier.

Liberté totale sur le rendu

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} />

Ce que ça ne remplace pas

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.

Conclusion

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.