Blog Markdown (multilingue, avec tags)
Un blog où le contenu est constitué de fichiers, pas de lignes de base de données. L'ensemble de la conception repose sur une idée :
l'identité d'un article est sa translationKey, pas son slug. Les slugs sont localisés
pour le référencement et diffèrent selon la langue ; la clé est ce qui fait de trois fichiers un seul article,
et c'est ce qui alimente le hreflang, le sélecteur de langue, les articles connexes et
les redirections inter-locales. Obtenez ce champ correctement et tout le reste n'est que des recherches.
Écrit par l'ingénieur qui a livré ce module. L'implémentation précédente
contre laquelle il a été audité était un blog marketing multilingue généré statiquement
avec son sitemap et sa surface SEO. Les faits et règles ci-dessous sont ce que
la suite de couche de contenu et la build vérifient ; references/provenance.md contient
le compte rendu.
Quand l'utiliser
- Contenu rédigé en Markdown dans le dépôt, déployé avec l'application
- Plus d'une langue, avec des slugs différents par langue
- Pages de tags, articles connexes, flux ou hreflang sont dans le périmètre
- Vous voulez que le tout soit généré statiquement au moment de la build
Quand NE PAS l'utiliser
| Au lieu de cela | Utilisez |
|---|---|
| Un CMS hébergé est la source de vérité | le SDK de ce CMS ; ne gardez que references/content-model.md |
| Un site de documentation / centre d'aide (catégories, barre latérale, recherche) | une compétence de centre d'aide — modèle de navigation différent |
| Un blog monolingue de moins de 10 articles | fs simple + gray-matter en ligne ; ceci est un surcoût |
| Rendre du Markdown soumis par l'utilisateur | un moteur de rendu avec assainissement ; voir la note XSS dans references/rendering.md |
Architecture
content/blog/<locale>/<slug-localisé>.md # frontmatter + corps
|
v
parseFrontmatter --YAML, avec repli sur un analyseur ligne par ligne en cas de guillemets malformés
|
v
getAllPosts(locale) <-- mémorisé par locale en production
|
+--> translationKey ----> alternatives / hreflang / changement de langue
+--> tags -> SLUG du tag ---> pages de tags (le slug est l'identité, pas le libellé)
+--> related (slugs EN) -> articles connexes, avec un repli EN
|
v
/blog /blog/[slug] /blog/tag/[slug] sitemap.xml feed.xml
Faits critiques
- Les relations sont rédigées une fois, dans la locale par défaut, sous forme de slugs de la locale par défaut. Un fichier traduit peut omettre
relatedentièrement et obtiendra tout de même les bons articles connexes, résolus viatranslationKey, de sorte que chaque traduction rend la même section connexe. Le test de repli le vérifie. - L'identité d'un tag est son slug, pas son libellé.
"IA Agents"et"IA agents"produisent le même slug d'URL. Regroupez par slug et associez les articles par slug, de sorte que toutes les variantes orthographiques apparaissent sur la même page de tag. Deux tests le vérifient. gray-matterlève une exception sur un scalaire YAML entre guillemets contenant des caractères de guillemets bruts — les guillemets typographiques dans un extrait traduit le font. Sans un analyseur de repli, la build s'arrête sur ce fichier ; l'analyseur ligne par ligne le garde fonctionnel. Vérifié par rapport à la bibliothèque et couvert par un test.gray-mattermet en cache un échec d'analyse comme un résultat vide. Il écrit son entrée de cache avant d'analyser, donc la deuxième analyse et toutes les suivantes d'un fichier qui a levé une exception renvoient{}— aucune erreur, aucun frontmatter. Appelez toujoursmatter(contents, {}); tout objet d'options désactive le cache.- Le chargement est O(articles x locales) par page sauf si vous mémorisez. Mémorisez par locale pour que chaque fichier de contenu soit lu une fois par build ; la résolution des alternatives dans
generateMetadataest autrement une amplification de lecture de fichier de 71x, mesurée sur l'implémentation précédente. getPostSlugsdoit filtrer.md. Le filtre empêche un fichier.DS_Storeerrant de devenir une lecture.DS_Store.mdet une erreurENOENT. Un test le vérifie.- Les pages de tags n'ont pas d'identité inter-locale. Les tags sont du texte libre par locale, donc le sélecteur de langue doit se rabattre sur l'index du blog pour une URL de tag plutôt que de construire une URL qui aboutit à une 404.
Règles strictes
Ne jamais utiliser le slug comme clé pour les recherches inter-locales. Les slugs sont localisés intentionnellement.
translationKeyest la colonne de jointure ; une correspondance de slug entre locales est une coïncidence, pas une relation.
Ne jamais laisser
getPostBySlugdécider implicitement de la visibilité des brouillons. Filtrez les brouillons dans le chargeur ;dynamicParams = falsene les cache que comme effet secondaire et cesse de le faire dès que le drapeau change. Le test des brouillons applique le filtre.
Ne jamais afficher une constante « 5 min de lecture ». Calculez-la à partir du corps ; cela coûte un
split, et le test du temps de lecture le vérifie.
Ne jamais déduire l'URL d'une page de tag à partir du libellé au moment de la lecture et le libellé à partir de l'URL au moment du rendu sans une vérification de collision. Cet aller-retour n'est sans perte que tant que chaque libellé produit un slug unique.
Démarrage rapide
- Définissez le contrat de locale et le schéma frontmatter — content-model.md
- Construisez le chargeur (mémorisé, filtré
.md, analyseur de repli) — content-loader.md - Branchez les locales, les alternatives et les redirections inter-locales — i18n-and-routing.md
- Ajoutez le regroupement par tags et les pages de tags — tags.md
- Construisez les trois routes, les métadonnées et JSON-LD — pages-and-seo.md
- Affichez les corps et les couvertures — rendering.md
- Ajoutez le script de validation et lisez les lacunes opérateur — operations.md
- Exécutez les fixtures et les tests — testing.md
Vous portez ceci dans une application existante ? Remplissez d'abord le tableau des points d'insertion dans adaptation.md — cela prend dix minutes et vous évite un renommage.
Répertoire des références
| Scénario | Mots-clés déclencheurs | Référence |
|---|---|---|
| Champs frontmatter, brouillons, clés de traduction, relations | frontmatter, schéma, translationKey, brouillon, related, extrait | content-model.md |
| Lecture de fichiers, mise en cache, échecs YAML, temps de lecture | gray-matter, fs, cache, mémoriser, ENOENT, YAMLException, analyser | content-loader.md |
| Locales, hreflang, alternatives, liens vers la mauvaise locale | i18n, locale, hreflang, alternatives, canonique, sélecteur de langue, redirection | i18n-and-routing.md |
| URL de tags, collisions, pages de tags fines | tag, slug, diacritiques, collision, page de tag, taxonomie | tags.md |
| Les routes, métadonnées, JSON-LD, sitemap, RSS | page.tsx, generateStaticParams, generateMetadata, dynamicParams, sitemap, RSS, flux, BlogPosting | pages-and-seo.md |
| Markdown vers React, blocs de code, images de couverture | react-markdown, remark-gfm, coloration syntaxique, couverture, motif, accent, XSS | rendering.md |
| Validation du contenu, coût de la build, flux de travail de l'éditeur | validation, vérification CI, temps de build, brouillons, rédaction, opérateur | operations.md |
| Fixtures et la suite de tests passante | test, fixture, vitest, node:test, régression, assert | testing.md |
| Intégration dans une application hôte : points d'insertion, renommages, sonde | adapter, porter, hôte, point d'insertion, renommer, CMS, intégrer | adaptation.md |
| Le registre d'audit : ce qui a changé, a été conservé et a été ajouté par rapport à l'implémentation précédente | provenance, écarts, fidélité, audit, registre | provenance.md |