Avertissement au lecteur
Ceci est le premier article en français de ce blog. Le reste du corpus technique est en anglais — conventions du métier, audience internationale, identifiants C# toujours en anglais.
Mais la thèse que porte cette série a besoin du mot Aufhebung. En anglais, on traduit par sublation — un mot que personne n'utilise en dehors des départements de philosophie. En français, on peut écrire suppression-conservation-élévation et le lecteur entend les trois temps dans une seule phrase. C'est pourquoi cette série est en français. Le lecteur anglophone trouvera les renvois vers les articles techniques en anglais, qui restent la référence pour le code.
La non-identité : trois niveaux
Ce site transforme du markdown en HTML. C'est une affirmation banale — tout générateur de sites statiques fait ça. Mais il y a quelque chose de non-trivial dans cette transformation : le markdown n'est pas le HTML, et le HTML n'est pas le markdown. Cette non-identité n'est pas un accident — c'est la condition de possibilité du pipeline.
Elle opère sur trois plans distincts :
| Dimension | Côté markdown | Côté HTML | Ce qui diffère |
|---|---|---|---|
| Ontologique | Artefact d'auteur : fichier texte, édité dans VS Code, versionné en git | Artefact de lecteur : page web, interprétée par un navigateur, indexée par Google | Mode d'existence |
| Informationnel | Frontmatter YAML, directives mermaid, liens relatifs .md |
Meta tags, JSON-LD, TOC sidebar, slugs hiérarchiques, liens .html |
Contenu (ajouté ET perdu) |
| Opérationnel | Sert à écrire (auteur, éditeur, commit) | Sert à lire (lecteur, navigateur, clic) | Rôle dans le workflow |
Niveau 1 — Ontologique : des espèces d'être différentes
Le fichier que vous ne voyez pas en ce moment s'appelle content/blog/non-identite-md-html/01-la-these.md. Il existe sur un disque, dans un dépôt git, avec un historique de commits. Il a été ouvert dans un éditeur de texte, modifié, sauvegardé, poussé sur une branche. C'est un artefact d'auteur — son mode d'existence est celui du code source.
Ce que vous voyez en ce moment est un fichier .html servi par Vercel. Il a été interprété par votre navigateur, rendu avec des polices, coloré par un thème CSS, indexé par un crawler. C'est un artefact de lecteur — son mode d'existence est celui de l'interface.
Ces deux fichiers ne sont pas le même objet. Ils n'ont pas le même format, pas le même chemin, pas le même cycle de vie, pas le même public. Le markdown commence par un bloc de frontmatter YAML que le lecteur ne voit jamais :
---
title: "Partie 1 : La thèse — Markdown n'est pas HTML, et réciproquement"
section: blog
order: 1
parent: non-identite-md-html
date: "2026-04-10"
tags: ["MDE", "MOF", "Aufhebung", "Hegel", "Ontologie", "Non-identité", "Français"]
------
title: "Partie 1 : La thèse — Markdown n'est pas HTML, et réciproquement"
section: blog
order: 1
parent: non-identite-md-html
date: "2026-04-10"
tags: ["MDE", "MOF", "Aufhebung", "Hegel", "Ontologie", "Non-identité", "Français"]
---Ce bloc n'existe pas dans le HTML. Il a été consommé par le pipeline de build — transformé en balises <meta>, en données JSON-LD, en position dans la table des matières. Le frontmatter est la face cachée du markdown : structurante mais invisible.
Niveau 2 — Informationnel : une transformation non-bijective
La transformation markdown → HTML n'est ni injective ni surjective. Des informations sont ajoutées pendant la transformation, et des informations sont perdues.
Ce qui est ajouté (absent du markdown, présent dans le HTML) :
<!-- Généré par buildMetaTags() dans page-renderer.ts -->
<title>Partie 1 : La thèse — Markdown n'est pas HTML — Stéphane Erard</title>
<meta name="description" content="Trois niveaux de non-identité...">
<meta property="og:type" content="article">
<meta property="og:title" content="Partie 1 : La thèse...">
<meta property="article:published_time" content="2026-04-10">
<link rel="canonical" href="https://stephane-erard.com/content/blog/..."><!-- Généré par buildMetaTags() dans page-renderer.ts -->
<title>Partie 1 : La thèse — Markdown n'est pas HTML — Stéphane Erard</title>
<meta name="description" content="Trois niveaux de non-identité...">
<meta property="og:type" content="article">
<meta property="og:title" content="Partie 1 : La thèse...">
<meta property="article:published_time" content="2026-04-10">
<link rel="canonical" href="https://stephane-erard.com/content/blog/..."><!-- Généré par buildJsonLd() dans page-renderer.ts -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Partie 1 : La thèse...",
"datePublished": "2026-04-10",
"author": { "@type": "Person", "name": "Stéphane Erard" }
}
</script><!-- Généré par buildJsonLd() dans page-renderer.ts -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Partie 1 : La thèse...",
"datePublished": "2026-04-10",
"author": { "@type": "Person", "name": "Stéphane Erard" }
}
</script>Aucune de ces balises n'existe dans le fichier markdown. Elles sont créées par les fonctions buildMetaTags() et buildJsonLd() de page-renderer.ts à partir du frontmatter et de la configuration du site. C'est de l'information nouvelle — le HTML sait des choses que le markdown ne sait pas.
Ce qui est perdu (présent dans le markdown, absent du HTML) :
- Le frontmatter YAML brut (transformé en meta tags, mais la forme YAML disparaît)
- La structure en fichiers et dossiers (
content/blog/non-identite-md-html/01-la-these.md→ un seul.htmlplat) - Les directives mermaid (
%% @no-max-height) — consommées par le renderer, absentes du SVG final - Les liens relatifs en
.md— réécrits en.htmlparrewriteLinks()
On ne peut pas reconstruire le markdown original à partir du HTML. Et on ne peut pas prédire le HTML complet à partir du markdown seul (il faut le toc.json, le template, la configuration du site). La transformation est non-bijective dans les deux sens.
Niveau 3 — Opérationnel : des rôles différents
Le markdown sert à écrire. Il est optimisé pour l'auteur : syntaxe légère, édition dans un IDE, diff lisible en git, commit atomique. Quand j'écris ## L'Aufhebung, je pense au contenu, pas au rendu.
Le HTML sert à lire. Il est optimisé pour le lecteur : rendu typographique, navigation par le TOC, thème dark/light, copie de code en un clic, métadonnées pour le partage sur les réseaux sociaux.
Le même humain — moi — joue les deux rôles. Mais jamais en même temps. Quand j'écris dans VS Code, je suis dans le monde M1. Quand j'ouvre le navigateur pour vérifier le rendu, je suis dans le monde M0. Le pipeline de build (npm run build) est la frontière matérielle entre les deux mondes. C'est le moment où M1 devient M0 — où l'artefact d'auteur est supprimé, conservé, et élevé en artefact de lecteur.
L'Aufhebung : supprimé, conservé, élevé
Le concept hégélien d'Aufhebung a trois sens simultanés : supprimer (aufheben au sens de « mettre de côté »), conserver (aufheben au sens de « garder »), et élever (aufheben au sens de « porter plus haut »). Le mot allemand contient les trois — c'est pourquoi il est intraduisible.
La transformation markdown → HTML est une Aufhebung au sens technique :
Supprimé : le fichier .md n'est pas servi au lecteur. Le format markdown disparaît. Le frontmatter YAML disparaît. Les directives mermaid disparaissent. Le lien [texte](./foo.md) disparaît.
Conservé : le contenu textuel est là. Les mots que l'auteur a écrits sont les mots que le lecteur lit. La structure en headings est préservée (un ## markdown devient un <h2> HTML). L'intention de l'auteur traverse la transformation.
Élevé : des capacités nouvelles apparaissent. Le HTML a une navigation (TOC sidebar). Il a du SEO (meta tags, JSON-LD, sitemap). Il a de l'accessibilité (ARIA, contrastes). Il a des thèmes (dark/light). Il a des diagrammes rendus en SVG (au lieu de texte mermaid brut). Il a des URLs canoniques (au lieu de chemins relatifs).
Le cas le plus concret est la réécriture des liens. Dans le markdown, un lien interne ressemble à ceci :
[Modeling](../modeling.md)[Modeling](../modeling.md)Après passage dans rewriteLinks() de page-renderer.ts, il devient :
<a href="/content/blog/modeling.html">Modeling</a><a href="/content/blog/modeling.html">Modeling</a>Le lien est supprimé (le format relatif .md disparaît), conservé (la même destination est atteinte), et élevé (l'URL est canonique, navigable, indexable). Trois opérations en une — exactement l'Aufhebung.
Le cadre MOF : M1 → M0
En termes de l'ingénierie dirigée par les modèles (MDE), ce que je viens de décrire a un nom précis. Le standard MOF (Meta-Object Facility) de l'OMG définit quatre niveaux d'abstraction — voir le post Modeling, Metamodeling, and Meta-Metamodeling in C# pour le détail de la pile complète.
Instanciée sur le pipeline de ce site, la pile donne :
- M3 est la grammaire des grammaires : la spécification CommonMark (qui définit ce qu'est un heading, un lien, un bloc de code) et la spécification YAML (qui définit ce qu'est un frontmatter).
- M2 est le métamodèle de ce site : le schéma attendu du frontmatter (quels champs, quelles valeurs), les conventions de contenu (catégories, séries, tooltips), et le pipeline de build lui-même (
build-toc.ts,page-renderer.ts,mermaid-renderer.ts). - M1 est le modèle : les fichiers markdown que j'écris. Chaque fichier
.mdest une instance du métamodèle M2 — il respecte le schéma frontmatter, utilise les conventions de contenu, et sera transformé par le pipeline. - M0 est l'instance : les fichiers HTML servis au lecteur. Chaque
.htmlest le résultat de la transformation M1→M0 opérée par le pipeline M2.
La non-identité est entre M1 et M0. Le pipeline M2 est le compilateur qui médie cette non-identité — il la rend productive au lieu de la subir.
Pourquoi ça compte
Ce n'est pas un exercice philosophique gratuit. Chaque décision d'architecture du pipeline découle de cette non-identité :
- La validation des liens (
validate-md-links.ts) opère au niveau M1 — elle vérifie que les fichiers.mdcibles existent avant la transformation, parce qu'après (au niveau M0) les liens ont changé de forme. - La génération du TOC (
build-toc.ts) est une extraction sémantique : elle lit tous les M1 pour produire un artefact M0 (letoc.json) qui n'existe dans aucun fichier source individuel. - Le nettoyage des orphelins (
pruneOrphanHtml()) maintient l'invariant que chaque M0 a un M1 correspondant — si le markdown source disparaît, le HTML généré aussi. - Le rendu Mermaid est une non-identité au carré : le texte mermaid (M1) est transformé en SVG (M0) par un sous-compilateur imbriqué dans le compilateur principal.
La partie 2 entre dans le détail de chaque étape du compilateur, avec le code source réel.