Traiter Markdown et MDX en sécurité avec Claude Code
Traiter Markdown/MDX avec Claude Code: AST, frontmatter, XSS, liens et QA locale.
Markdown n’est pas un simple bloc de texte
Un article Markdown ou MDX publié contient plus que des paragraphes. Il porte du frontmatter, une description SEO, des niveaux de titres, des ancres générées, des blocs de code, des liens internes, des liens officiels, des routes par langue et parfois du HTML brut. Si l’on demande à Claude Code de “rendre l’article meilleur” sans contrat de traitement, le texte peut devenir plus fluide tout en cassant un slug, en supprimant un CTA, en changeant l’image hero ou en laissant une locale comme résumé trop court.
La méthode fiable consiste à séparer rédaction et vérification. Claude Code peut réécrire, localiser et enrichir. La structure doit rester contrôlée par des scripts. Pour Markdown et MDX, on lit les fichiers avec un AST, c’est-à-dire un arbre de syntaxe abstraite. Le frontmatter se valide comme des données. Le HTML doit passer par une étape explicite de sanitization. Les dix fichiers de locale doivent être contrôlés ensemble.
Les références principales ont été vérifiées le 2 juin 2026. Le guide unified décrit le pipeline parse, transform et stringify. La page syntax trees explique pourquoi un AST est plus sûr qu’une lecture ligne par ligne. Markdown passe par remark et remark-parse. La syntaxe MDX est couverte par la documentation MDX. Pour le frontmatter, gray-matter est un choix courant. Pour le HTML et le XSS, comparez rehype-sanitize avec la cheatsheet OWASP XSS. Pour cadrer l’agent, lisez aussi Claude Code overview et settings.
flowchart LR
A["Fichier MDX"] --> B["frontmatter"]
B --> C["validation schema"]
A --> D["AST remark / MDX"]
D --> E["titres, fences, liens"]
D --> F["pipeline rehype"]
F --> G["sanitization"]
C --> H["locale et build checks"]
E --> H
G --> H
Choisir le bon parser avant de coder
La première consigne donnée à Claude Code doit nommer les outils. Dire seulement “parse Markdown” pousse souvent vers une regex courte. Cela suffit pour un fichier jouet, pas pour un article publié.
| Besoin | Bon choix | Raccourci fragile |
|---|---|---|
| Lire titres, liens et code fences | remark-parse avec parcours AST | Regex ^## sur le texte |
Gérer du JSX dans .mdx | remark-mdx ou le compiler MDX | Parser Markdown seul |
| Produire du HTML | remark-rehype vers rehype | Concaténer des strings HTML |
| Autoriser du HTML brut | rehype-raw puis rehype-sanitize | allowDangerousHtml seul |
| Lire frontmatter | gray-matter plus validation | Split YAML à la main |
L’AST distingue le sens. Un ## faux titre dans un bloc de code ne doit pas entrer dans la table des matières. Une URL dans les props d’un composant MDX n’a pas toujours le même rôle qu’un lien éditorial. Un tags: Claude Code, Markdown est une chaîne, pas un tableau. Ces problèmes se détectent mieux avec parser et schema qu’avec une relecture visuelle.
Quatre cas d’usage concrets
Le premier cas est la mise à jour d’un article déjà publié. Il faut revoir title, description, updatedDate, liens officiels, liens internes, exemples de code et CTA. Pour ClaudeCodeLab, on peut relier le sujet aux bonnes pratiques CLAUDE.md et au web scraping avec Claude Code, sans modifier d’autres slugs.
Le deuxième cas est une documentation qui utilise des composants MDX. Callouts, onglets, cartes de prix, FAQ et exemples vivants sont pratiques, mais ils mélangent Markdown et JSX. Un checker qui ne comprend pas MDX casse vite les composants.
Le troisième cas est la publication multilingue. Un canonical japonais complet ne suffit pas si les versions française, espagnole, portugaise ou indonésienne deviennent de simples résumés. Chaque locale doit contenir cas d’usage, pièges, snippets exécutables, liens officiels, liens internes, CTA et note de vérification.
Le quatrième cas est l’exploitation commerciale du contenu. Pages Gumroad, pages de formation, ressources gratuites et emails réutilisent souvent Markdown. Plus la page est proche d’un achat ou d’une demande de conseil, plus les code fences, les liens et le HTML doivent être vérifiables.
Installation minimale à copier
Les snippets ci-dessous utilisent Node.js 18 ou plus et des modules ESM. Testez-les dans un dossier isolé avant de les brancher sur le dépôt.
mkdir mdx-audit-demo
cd mdx-audit-demo
npm init -y
npm pkg set type=module
npm install unified remark-parse remark-mdx remark-gfm gray-matter
npm install unist-util-visit github-slugger
npm install remark-rehype rehype-raw rehype-sanitize rehype-stringify
mkdir tools
Exemple 1: auditer frontmatter, titres, fences et liens
Ce script lit le frontmatter avec gray-matter, parse le body avec remark et le support MDX, puis vérifie les champs obligatoires, la longueur de description, les langues de code fences et la présence de liens internes et externes.
// tools/audit-mdx.mjs
import fs from "node:fs/promises";
import matter from "gray-matter";
import GithubSlugger from "github-slugger";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkMdx from "remark-mdx";
import remarkGfm from "remark-gfm";
import { visit } from "unist-util-visit";
const file = process.argv[2];
if (!file) {
throw new Error("Usage: node tools/audit-mdx.mjs article.mdx");
}
const source = await fs.readFile(file, "utf8");
const { data, content } = matter(source);
const errors = [];
const links = { internal: [], external: [] };
const headings = [];
const codeBlocks = [];
for (const key of ["title", "description", "pubDate", "heroImage", "lang"]) {
if (typeof data[key] !== "string" || data[key].trim() === "") {
errors.push(`frontmatter.${key} is required`);
}
}
if ([...String(data.description ?? "")].length > 120) {
errors.push("description must be 120 characters or fewer");
}
if (!Array.isArray(data.tags) || data.tags.length === 0) {
errors.push("frontmatter.tags must be a non-empty array");
}
const tree = unified()
.use(remarkParse)
.use(remarkMdx)
.use(remarkGfm)
.parse(content);
const slugger = new GithubSlugger();
visit(tree, (node) => {
if (node.type === "heading") {
const text = plainText(node);
headings.push({ depth: node.depth, text, slug: slugger.slug(text) });
}
if (node.type === "code") {
codeBlocks.push({ lang: node.lang || "", meta: node.meta || "" });
if (!node.lang) errors.push("code fence is missing a language");
}
if (node.type === "link") {
const url = String(node.url || "");
if (url.startsWith("http")) links.external.push(url);
if (url.startsWith("/")) links.internal.push(url);
}
});
if (links.internal.length === 0) errors.push("missing internal link");
if (links.external.length === 0) errors.push("missing external link");
if (errors.length > 0) {
console.error(errors.map((error) => `- ${error}`).join("\n"));
process.exit(1);
}
console.log(JSON.stringify({ headings, codeBlocks, links }, null, 2));
function plainText(node) {
if (typeof node.value === "string") return node.value;
if (!Array.isArray(node.children)) return "";
return node.children.map(plainText).join("");
}
node tools/audit-mdx.mjs site/src/content/blog-fr/example.mdx
Exemple 2: convertir Markdown en HTML sûr
Si vous n’avez pas besoin de HTML brut, ne l’activez pas. Si vous devez l’accepter, parsez-le puis sanitizez-le immédiatement. allowDangerousHtml seul n’est pas une mesure de sécurité.
// tools/markdown-to-safe-html.mjs
import fs from "node:fs/promises";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import remarkRehype from "remark-rehype";
import rehypeRaw from "rehype-raw";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import rehypeStringify from "rehype-stringify";
const file = process.argv[2];
const markdown = await fs.readFile(file, "utf8");
const schema = {
...defaultSchema,
attributes: {
...defaultSchema.attributes,
code: [["className", /^language-/]],
},
};
const html = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize, schema)
.use(rehypeStringify)
.process(markdown);
console.log(String(html));
L’ordre est essentiel. rehype-raw remet le HTML brut dans l’arbre HTML, puis rehype-sanitize supprime les tags et attributs non autorisés. Sans cette deuxième étape, une valeur dangereuse peut arriver jusqu’au DOM.
Exemple 3: vérifier les dix locales
Ce script confirme que le même slug existe dans chaque langue, que heroImage est conservé, que updatedDate est correct et que chaque body contient des liens internes et externes.
// tools/check-locales.mjs
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";
const slug = "claude-code-markdown-processing.mdx";
const expectedHero = "/images/hero/hero-077.png";
const locales = [
["ja", "site/src/content/blog"],
["en", "site/src/content/blog-en"],
["zh", "site/src/content/blog-zh"],
["ko", "site/src/content/blog-ko"],
["es", "site/src/content/blog-es"],
["fr", "site/src/content/blog-fr"],
["de", "site/src/content/blog-de"],
["pt", "site/src/content/blog-pt"],
["hi", "site/src/content/blog-hi"],
["id", "site/src/content/blog-id"],
];
const errors = [];
for (const [lang, dir] of locales) {
const file = path.join(dir, slug);
const source = fs.readFileSync(file, "utf8");
const { data, content } = matter(source);
if (data.lang !== lang) errors.push(`${lang}: lang mismatch`);
if (data.heroImage !== expectedHero) errors.push(`${lang}: hero changed`);
if (data.updatedDate !== "2026-06-02") {
errors.push(`${lang}: updatedDate mismatch`);
}
if ([...String(data.description ?? "")].length > 120) {
errors.push(`${lang}: description too long`);
}
if (!content.includes("https://")) errors.push(`${lang}: no external link`);
if (!content.includes("](/")) errors.push(`${lang}: no internal link`);
}
if (errors.length > 0) {
console.error(errors.map((error) => `- ${error}`).join("\n"));
process.exit(1);
}
console.log("locale set is consistent");
Pièges concrets
| Échec | Résultat | Garde-fou |
|---|---|---|
| Lire les titres avec regex | Les faux titres dans le code entrent dans le sommaire | Parcourir les noeuds heading |
tags devient une string | Filtres et articles liés cassent | Valider les types frontmatter |
| Slugs incohérents | Ancres cassées entre langues | Utiliser le même slugger |
| Faire confiance au HTML brut | Risque XSS par attributs ou tags | Sanitizer avec schema |
| Ne pas vérifier les liens externes | Documentation officielle déplacée | Tester avant publication |
| Prompt trop large | Fichiers d’autres workers modifiés | Fixer owned_files |
Ces pièges doivent être écrits dans la demande. Claude Code répond mieux à des contraintes vérifiables qu’à une consigne vague comme “améliore la qualité”.
Prompt sûr pour Claude Code
task: "Refresh one published MDX article"
owned_files:
- "site/src/content/blog-fr/claude-code-markdown-processing.mdx"
preserve:
- "slug path"
- "heroImage"
- "unrelated dirty files"
required:
- "updatedDate: 2026-06-02"
- "description <= 120 characters"
- "AST-based Markdown checks"
- "official external links"
- "internal links and monetization CTA"
forbidden:
- "regex-only heading parsing"
- "raw HTML without sanitization"
- "thin locale summaries"
verification:
- "node scripts/check-code-fences.mjs"
- "node scripts/check-updated-article-quality.mjs"
Vérification avant publication et CTA
Avant publication, combinez scripts locaux et lecture humaine. Les scripts contrôlent structure, metadata, fences, liens et profondeur. La personne vérifie le naturel de la traduction, la lecture mobile, l’intention de recherche et le CTA.
node tools/audit-mdx.mjs site/src/content/blog-fr/claude-code-markdown-processing.mdx
node tools/check-locales.mjs
node scripts/check-code-fences.mjs
node scripts/check-updated-article-quality.mjs
Pour démarrer seul, utilisez la cheatsheet Claude Code gratuite. Pour des prompts réutilisables de review et rédaction, prenez les templates de prompts Claude Code. Pour une équipe qui veut permissions, CI, workflow multilingue et revue éditoriale, passez par le training et conseil Claude Code.
Résultat de vérification pratique
Dans cette mise à jour, Masa a traité l’article comme une chaîne de publication réelle: description courte, updatedDate, conservation de heroImage, langues des code fences, liens officiels, profondeur des locales et CTA. L’audit basé sur AST couvre des erreurs qu’une regex ne voit pas, surtout les titres dans les blocs de code et la syntaxe MDX près des composants. Les commandes finales sont node scripts/check-code-fences.mjs et node scripts/check-updated-article-quality.mjs. La leçon est nette: Claude Code devient fiable quand le contrat de l’article est exécutable, pas seulement quand le prompt demande une meilleure rédaction.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Échelle de sécurité des permissions Claude Code
Passer du read-only aux éditions limitées, preuves et checks de déploiement sans perdre le contrôle.
Claude Code Small PR Proof Pack : rendre les petits changements reviewables
Un pack de preuve pour PR Claude Code : diff, vérifications, URL publique, CTA et rollback.
Gate de review avant commit avec Claude Code
Review avant commit avec Claude Code : diff, build, URL publique, liens Gumroad, CTA consultation, tests manquants et fichiers hors scope.