12 — Glossaires et annuaires : les formats glossary et directory
Statut : accepté · Phase : P14 · Mis à jour : 24/10/2026.
Idée d'origine (sites/plans/pages/archive/BACKLOG_IDEA.md) : « glossaire.recit.dev |
annuaire.recit.dev — Markdown faisant l'annuaire de site internet, ou d'adresse, ou autre glossaire
». Ce document la pousse jusqu'à une spécification du même niveau que 08-formats.md. Il propose
des décisions, chacune avec une recommandation, et ne modifie aucun autre plan : les changements à
reporter ailleurs sont listés au §9. Les questions 1, 2, 3, 5, 6, 7 et 8 du §8 ont été tranchées le
24/10/2026, sans l'utilisateur et à sa demande (décisions réversibles).
Placement, ports, domaines et règles communes aux plans 10 à 14 : 06-roadmap.md, section « Backlog
: plans 10 à 14 ». Ce plan s'y conforme. En résumé : un lot avancé en P9 (schémas, liste « awesome »
lue comme annuaire, vue A–Z minimale), puis P14 Directory après la 1.0. Ce plan est aussi
propriétaire de deux éléments partagés : le type Place et la commande recit geocode (qui
n'existe pas en v1 : les coordonnées sont écrites par l'auteur).
1. L'idée en une phrase, et ce qu'elle apporte à Récit
Une liste de termes ou d'adresses s'écrit une fois en Markdown, puis elle se lit partout : en page A–Z, en infobulles dans toutes les autres stories, en annexe de livre, en slide, en carte de story, en carte géographique, en JSON-LD pour les moteurs.
Les deux idées du backlog (glossaire et annuaire) sont la même chose vue de deux côtés : une collection d'entrées nommées. Un glossaire associe un terme à une définition. Un annuaire associe une entité (site, lieu, organisation) à des coordonnées. Le modèle, le tri, les filtres, la recherche, les ancres et les données structurées sont communs, la mise en page change.
Ce que cela apporte à Récit, au-delà d'un dixième et d'un onzième format :
- Une fonction transverse, pas seulement un format. Les termes d'un glossaire se lient tout
seuls dans un deck, une page de doc, un rapport ou un livre (
:term). C'est la première fois qu'une source Récit enrichit les autres sources au lieu d'être seulement rendue. C'est la forme la plus littérale de « écrire une fois, raconter partout » : la définition est écrite à un seul endroit et apparaît partout où le mot apparaît. - Le glossaire de Récit lui-même.
08-formats.md§9 promet déjà que « la doc a un glossaire » pour éviter la confusionStory/stories. Ce glossaire devient le premier exemple réel du format, publié sur docs.recit.dev et lié automatiquement dans toute la doc, comme la roadmap de Récit est le premier exemple du formatroadmap. - Un pont vers les données et les lieux. Un annuaire est une source de données (
@recit/data: compter les entrées par catégorie sans écrire de chiffre), une source de lieux (calendar:location=@lieu, cartes : vue carte) et une source de cartes de story (un lieu = une carte). - Une compatibilité avec l'existant. Une liste « awesome » (
- [Nom](url) - Description.) est déjà un annuaire valide, sans rien changer. Un auteur passe à Récit sans réécrire.
2. Ce que la recherche a appris
2.1 État de l'art
| Outil | Syntaxe | Lien automatique | Ce qu'on retient |
|---|---|---|---|
Material for MkDocs, abbr + auto_append | *[HTML]: Hyper Text Markup Language dans un fichier ajouté à chaque page | oui, toutes les occurrences | un fichier de termes unique pour tout le projet |
| mkdocs-ezglossary | liste de définitions section:term, référence <section:term> | non, explicite | plusieurs glossaires, page récapitulative avec rétroréférences (« où ce terme est utilisé »), sortie PDF |
| docusaurus-plugin-glossary | JSON de termes + <GlossaryTerm term> | oui, plugin remark sans casse, frontières de mot | le lien automatique ignore code, liens et composants MDX |
| GRNET / eSSIF-Lab terminology | %%term|term%%, une page par terme | non | contre-exemple : il réécrit les sources au build, ce qui est critiqué. Récit ne réécrit jamais une source au rendu |
| remark-abbr | *[TERM]: desc | oui | cassé avec micromark d'après la liste officielle des plugins remark : à ne pas adopter |
| remark-deflist / mdast-util-definition-list | Terme puis : définition | — | deux paquets aux nœuds incompatibles, maintenance incertaine : une dépendance de plus, écartée |
| awesome-lists + awesome-lint | H2 = catégorie, - [Nom](url) - Description. | — | c'est déjà un annuaire de sites en Markdown. Règle no-dead-urls |
Sources : https://squidfunk.github.io/mkdocs-material/reference/tooltips/, https://pypi.org/project/mkdocs-ezglossary-plugin/1.6.5, https://tomevault.io/tome/mcclowes/docusaurus-plugin-glossary, https://essif-lab.pages.grnet.gr/framework/docs/terminology-plugin-instructions, https://gitlab.grnet.gr/terminology/docusaurus-terminology/-/issues/7, https://github.com/remarkjs/remark/blob/HEAD/doc/plugins.md, https://unifiedjs.com/explore/package/remark-deflist/, https://npmjs.com/package/awesome-lint.
2.2 Ce que le dépôt fournit déjà (vérifié)
| Besoin | Déjà présent | Où |
|---|---|---|
Directives :term[…]{…}, :::entry | remark-directive 3.0.1 | packages/core/package.json |
| Parcours de l'arbre Markdown | unist-util-visit 5.1.0 | packages/core/package.json |
Attributs {k=v} en fin d'item | convention spécifiée pour roadmap | 08-formats.md §4 |
| Carte au survol accessible | @radix-ui/react-hover-card 1.1.23 (et tooltip, popover) | packages/site/package.json |
| Recherche plein texte | pagefind 1.5.2, plugin search-pagefind | packages/site |
| Import CSV / JSON / YAML | prévu dans @recit/data (P6) | 08-formats.md §0 |
| Index de fin de livre | :index[term] et back: [notes, index] dans book | 08-formats.md §3 |
JSON-LD Event avec un lieu | prévu dans calendar | 08-formats.md §5 |
llms.txt | généré par le fork | packages/site |
remark-directive-rehype 1.0.0 n'est déclaré que par packages/site (privé, qu'aucun package
publié n'importe) : il n'est pas disponible pour @recit/core. On ne s'en sert pas ; s'il devenait
nécessaire, il entrerait au catalogue pnpm à la même version.
Conséquence : aucune nouvelle dépendance n'est nécessaire. Tri, lettres et noms de pays passent
par Intl (Intl.Collator, Intl.DisplayNames), la vérification des liens par le fetch natif de
Node.
2.3 Données structurées
- Glossaire :
DefinedTermSetqui contient desDefinedTerm(name,description,termCode,inDefinedTermSet). Ces types sont dans la zone « new » de schema.org, et aucune page de Google Search Central ne documente de résultat enrichi pour eux. On émet le balisage pour l'interopérabilité, sans promettre de gain de référencement (règle « aucune métrique inventée »). https://schema.org/DefinedTermSet, https://schema.org/DefinedTerm. - Annuaire :
ItemList(positions à partir de 1) d'élémentsLocalBusinessou sous-type,OrganizationouWebSite, avecPostalAddress,GeoCoordinates,url,sameAs. Le carrousel Google est en bêta, n'accepte que LocalBusiness, Product et Event, demande au moins trois éléments pointant vers des URL distinctes du même domaine, avec le balisage visible sur la page. Une source secondaire le dit limité à l'EEE, à vérifier. https://developers.google.com/search/docs/appearance/structured-data/carousels-beta.
2.4 Accessibilité
Les lecteurs d'écran en mode navigation ignorent souvent les infobulles posées sur du texte, et un
attribut title seul n'est pas un support fiable. Le terme lié doit donc être un vrai lien
(<a href="/glossary#api">), qui marche sans JavaScript et à l'impression, avec une carte au survol
et au focus, fermée par Échap. Pas de contenu interactif dans un role=tooltip.
https://lightningdesignsystem.com/accessibility/patterns/tooltip,
https://getwcag.com/accessibility-guide/aria-tooltip-name.
2.5 Vie privée
Des coordonnées professionnelles qui identifient une personne physique (indépendant, profession libérale) sont des données personnelles. La CNIL a sanctionné AnnuaireFrancais.fr en 2021, notamment pour une adresse de domicile publiée comme adresse professionnelle. Les droits de rectification, d'effacement et d'opposition (RGPD art. 16, 17 et 21) s'appliquent. https://www.cnil.fr/fr/reutilisation-de-donnees-par-des-annuaires-en-ligne-quels-droits-pour-les-professionnels-concernes, https://next.ink/brief-article/la-cnil-inflige-3-000-euros-damende-a-lannuairefrancais-fr/.
2.6 Liens et géocodage
- Vérifier des liens externes en CI est instable par nature (sites lents, limites de débit). lychee
est un binaire Rust, donc une seconde stack : on copie ses bonnes pratiques (cache avec âge
maximal,
Retry-After) sans l'adopter. https://lychee.cli.rs/continuous-integration/github/. - Le Nominatim public interdit le géocodage en masse régulier, impose au plus une requête par seconde, un User-Agent identifiable et l'attribution OSM (ODbL). https://operations.osmfoundation.org/policies/nominatim/.
3. Décisions proposées
D1 — Un package, deux formats, une fonction transverse
| Option | Pour | Contre |
|---|---|---|
A. @recit/glossary et @recit/directory | noms évidents | modèle, A–Z, filtres, recherche et JSON-LD en double : contraire à la stack unique |
B. un seul format directory avec entries: glossary | un seul littéral | le lien automatique des termes (utile à tous les formats) se retrouve enfoui dans un format d'annuaire |
C. un package @recit/directory, deux formats, termes dans le cœur | un modèle, deux lectures, rien en double | deux littéraux de plus dans StoryFormat |
Recommandation : C.
@recit/directory(packages/formats/directory) porte le modèle communEntryet deux formats déclarés :format: glossary(A–Z et définitions) etformat: directory(fiches, liste, tableau, carte en option).- Le lien automatique des termes ne vit pas dans le format. Il va dans
@recit/components/terms(plugin remark qui produit un nœudtermneutre), avec son rendu (Term, carte au survol). Le cœur ne garde que le nomtermdans son registre et le schémaTermsConfig(règle B4 de06-roadmap.md). Un deck ou une page de doc profitent donc des termes sans installer@recit/directory, ce qui respecte « un package = une raison d'être installé seul ». - Le nom du package est
directory(le plus général), au singulier, commeroadmapetcalendar.
D2 — Une scène = une section (catégorie), l'entrée est l'unité du modèle
| Option | Effet |
|---|---|
| une scène = une entrée, chaque H2 coupe une scène | ajoute au cœur un second mécanisme de découpage (le H2 en plus de ---), qui contredit la syntaxe commune de 02-architecture.md §3 |
une scène = une entrée, --- entre chaque entrée | lourd pour un glossaire de plusieurs centaines de termes, et incompatible avec une liste « awesome » existante |
| une scène = une section, les entrées à l'intérieur | même logique que roadmap (une scène = une piste, les items dedans). --- reste le seul séparateur, et un fichier sans --- est une seule scène |
Recommandation : une scène = une section (une catégorie d'annuaire, ou un groupe thématique de
glossaire). Les entrées sont rangées sous la clé propre au format (scene.directory.entries), comme
le veut 02-architecture.md §2 : le cœur ne connaît aucun format. Les vues (A–Z, filtres)
travaillent sur les entrées, pas sur les scènes. Les ponts vers les formats « une chose à la fois »
(deck, stories, motion) choisissent explicitement leur unité : --per entry ou --per section.
C'est une correspondance fermée, jamais une devinette.
D3 — Syntaxe du glossaire
Une entrée par H2 dans une section. Les attributs de l'entrée sont en accolades à la fin du
titre, avec le même analyseur que les items de roadmap (un seul utilitaire dans @recit/core,
partagé). Le premier paragraphe est la définition courte (carte au survol, description du
JSON-LD, slide, carte de story). La suite est le corps complet.
Markdown
| Élément | Rôle |
|---|---|
--- id=… + H1 | une section (scène). Facultative : sans ---, tout le fichier est une section |
H2 + {…} | une entrée. id (obligatoire en mode strict, sinon dérivé du titre), aliases (formes reconnues par le lien automatique, pluriels compris), abbr, see (renvois), tags, termCode |
| premier paragraphe | définition courte, limitée par la règle de densité (avertissement au-delà du plafond de mots du thème) |
| reste du bloc | définition longue (page d'entrée, annexe de livre) |
source= + license= | provenance d'une définition reprise d'ailleurs (affichée en note, voir §5) |
Règles :
- deux entrées de même
id, un alias partagé par deux entrées, unseevers unidinconnu : erreur avec numéro de ligne (glossary.md:24 alias "lorems" already used by "lorem" (line 12)) ; - un terme jamais utilisé dans le projet : avertissement (si
usedIn: true) ; - aucune racinisation : les pluriels et variantes passent par
aliases, explicitement, parce qu'une règle de pluriel devinée ne marche que pour une langue.
Pas d'import MkDocs (*[HTML]: Hyper Text Markup Language) : tranché le 24/10 (question 8).
D4 — Les termes dans toutes les autres stories (:term)
Le glossaire se déclare au niveau du projet, dans recit.config.ts, sous une clé commune
terms (et non glossary, qui est la section du format) :
TypeScript
Dans n'importe quelle story (deck, docs, report, book, stories) :
Markdown
| Élément | Rôle |
|---|---|
terms (config) | sources de termes, mode de lien, page cible. Sans section, aucun lien automatique |
terms (frontmatter) | surcharge par story (autolink, ou terms: false) |
:term[texte]{id} | lien explicite, toujours rendu même si autolink: none |
:term[texte]{off} | refus local |
Règles du lien automatique, toutes testées :
- correspondance sans casse, sur des frontières de mot Unicode (
\p{L},\p{N}), l'alias le plus long d'abord (« dolor set » avant « dolor ») ; - ignorés : code (en ligne et blocs), liens existants, titres,
:termexistants, attributs, composants MDX, notes présentateur ; - par langue : une story en
frest liée avec les noms et alias deglossary.fr.md(§3, D7) ; :term{id=…}vers unidinconnu : erreur avec numéro de ligne ;- l'automate de correspondance est construit une fois par build et par langue, puis partagé.
Défaut v1 : none (tranché le 24/10, question 2) : seul un :term explicite crée un lien. Le
lien automatique vient ensuite, en option, livré en P14 au plus tôt, en mode first (première
occurrence par scène), parce qu'une slide ou une carte se lit seule. Une page de doc est une
scène, donc « première occurrence par page ». Dans tous les cas, rien n'est actif sans
terms.sources : déclarer la source vaut opt-in.
D5 — Syntaxe de l'annuaire
Compatible avec les listes « awesome » : H2 = catégorie, item de liste = entrée, {…} = champs
structurés, :::entry{id} = fiche longue.
Markdown
Une liste « awesome » brute, sans accolades, est lue en entries: links : id dérivé du nom,
catégorie = H2, description = texte après le tiret.
| Élément | Rôle |
|---|---|
H2 ou --- id=… | une catégorie (scène) |
- [Nom](url) — Résumé. {…} | une entrée. id, tags, category (sinon le H2), url, email, phone, street, city, postcode, region, country (ISO 3166-1 alpha-2), geo (lat,lon), sameAs, sponsored |
:::entry{id} | fiche longue d'une entrée, affichée sur sa page ou dépliée |
compagnon places.json | champs gérés par un outil (linkStatus, checkedAt, finalUrl, geo ajouté par une commande), le texte restant dans le .md (09-langues-et-json.md) |
Règles :
- l'adresse est structurée, jamais analysée :
street,city,postcode,countrysont des champs distincts. Une chaîne libreaddress="…"est acceptée pour l'affichage seulement, et le JSON-LDPostalAddressn'est émis qu'à partir des champs structurés. Découper une adresse libre serait une devinette ; geohors de [-90, 90] / [-180, 180],countryhors ISO, URL qui n'est pashttp(s): erreur avec numéro de ligne ;iden double,:::entryvers unidinconnu : erreur ;- entrée
sponsored: affichée avec une mention visible etrel="sponsored"(honnêteté envers le lecteur, et règle des moteurs de recherche).
entries en V1 : links, places, organizations. Réglages à plat dans le frontmatter
(settings = section directory de recit.config) ; la clé s'appelle entries et non kind,
réservé au discriminant des documents JSON (09-langues-et-json.md §2.2). Pas de people (tranché
le 24/10, vie privée, D10).
D6 — Schéma JSON typé
Dans @recit/core/schema (zod 4, zod/mini côté navigateur), JSON Schema généré par
z.toJSONSchema et publié sur recit.dev/schema/v1/glossary.json et /directory.json.
Ce plan est propriétaire de Place, le seul type de lieu de Récit. Il est défini une fois dans
@recit/core/schema et livré avec calendar (P10), son premier consommateur. Le plan 13 (cartes)
l'importe sans le redéfinir. Adresse structurée, geo facultatif ; une adresse libre
address n'existe que pour l'affichage, n'est jamais analysée et n'alimente jamais le JSON-LD.
Un champ location (événement de calendar, entrée d'annuaire, marqueur de carte) accepte trois
formes, résolues une seule fois par le même module : une chaîne libre (affichage seul, comme
location="Lorem Hall, Paris" dans 08-formats.md §5), une référence @id vers une entrée
d'annuaire, ou un Place inline (JSON). Ligne à reporter dans 08-formats.md §5 à P10.
TypeScript
Les noms exacts des vérifications (isoAlpha2, lat, lon) sont à écrire. Les scènes JSON des
deux formats, à ajouter au tableau de 09-langues-et-json.md §2.3 :
| Format | Une scène JSON contient | Exemple de champ localisable |
|---|---|---|
glossary | section : title, entries[] {id, name, aliases, summary, body, see, tags, abbr, source} | entries[].name, entries[].aliases, entries[].summary |
directory | catégorie : title, entries[] {id, name, url, summary, tags, place, email, phone, sponsored} | title, entries[].summary, place.name |
Règle de 09 §2.2 appliquée : url, geo, phone, email, country, postcode ne sont
jamais localisables. Le nom d'une organisation n'est pas traduit par défaut ; il peut l'être
(Localized) quand l'entité a réellement un nom dans chaque langue.
Équivalence (09 §2.4) : compile(md) == compile(toJson(md)) pour chaque exemple et chaque
fixture. recit convert places.md --to json, et dans l'autre sens --to md.
D7 — Langues
| Domaine | Règle (ligne à ajouter à 09-langues-et-json.md §5) |
|---|---|
| glossary | glossary.fr.md garde les mêmes id. Tri et lettres par Intl.Collator(locale, { sensitivity: "base" }) (« É » sous « E »), groupe « # » pour chiffres et symboles, initiale de l'écriture hors alphabet latin. Alias et lien automatique par langue |
| directory | catégories et résumés localisés. Noms de pays par Intl.DisplayNames(locale, { type: "region" }). Adresses affichées telles qu'écrites (il n'existe pas d'API Intl pour formater une adresse) |
Un terme sans traduction dans la langue de la page n'est pas lié automatiquement (on ne lie pas
un mot français vers une définition anglaise). C'est une exception explicite à la chaîne de
repli de 09-langues-et-json.md §3 (Localized → langue de repli → defaultLocale) : pour le lien
automatique seulement, il n'y a pas de repli. Elle est à citer dans la ligne glossary de 09 §5. Un
:term explicite, lui, suit le repli normal. recit i18n check signale les entrées non traduites
(avertissement), et les id absents d'une langue (erreur, comme pour les scènes).
D8 — Rendu dans les autres formats
On ne promet que des correspondances fermées.
| Vers | Glossaire | Annuaire | Phase |
|---|---|---|---|
docs | termes liés dans toutes les pages, <StoryEmbed format="glossary" view="az" />, page /glossary avec ancres | <StoryEmbed format="directory" view="cards" />, filtres dans l'URL | DI2, DI3 |
deck | ::glossary{terms=lorem,ipsum} : une slide de définitions. recit convert --to deck --per entry : un terme = une slide | ::directory{src=./places.md category=studios view=cards limit=6} : une slide de fiches | DI5 |
report | annexe « Glossary » générée avec les seuls termes utilisés dans le rapport | l'annuaire est une source de @recit/data : :value{data=places agg=count where="category=studios"}. Les chiffres sont calculés, jamais écrits | DI5 |
book | back: [glossary, index] : annexe glossaire, chaque terme suivi des pages où il apparaît (@recit/paged). Le glossaire (définitions) et :index (pages) restent deux notions distinctes | — | DI5 |
stories | « un mot par carte » : un terme = une carte (nom + définition courte) | un lieu ou un site = une carte (nom, résumé, ville, lien en CTA) | DI5 |
motion | un terme = un plan : nom → bloc title, définition courte → bloc statement. Une définition trop dense est une erreur, pas un résumé deviné | une entrée = un plan title + list (tags) | DI5 |
calendar | — | location=@dolor-hall dans un événement renvoie vers l'entrée (même type Place, même JSON-LD) | DI5 |
| cartes | — | view: map si le package de cartes (plan dédié) est installé, en dépendance optionnelle : liste et fiches n'en dépendent jamais | après le plan cartes |
toon, roadmap | rien de promis | rien de promis | — |
Sur papier (book, report PDF), la carte au survol n'existe pas : le terme reste un lien, et l'annexe porte la définition.
D9 — Navigation, recherche, vues
- Glossaire : rail A–Z (lettres sans entrée visibles mais désactivées), ancre stable par entrée
(
#lorem), page par entrée en option (pas par défaut), renvoissee, rétroréférences « Used in » calculées au build à partir des nœudstermde tout le projet. - Annuaire : vues
cards,list,table,map(optionnelle). Facettes dans l'URL (?category=studios&tag=design&city=Paris), comme les filtres deroadmap. - Recherche : Pagefind pour le plein texte du site (par langue, déjà prévu en
09§6), et un filtre instantané côté client sur un JSON des entrées (nom, alias, tags) produit au build. Pas de fuse.js ni de minisearch. - Poids : une page de doc ne charge que les définitions courtes des termes qu'elle contient,
pas tout le glossaire. Le composant
Termreste léger et n'importe aucun module lourd (règle « Bundle Size » deCLAUDE.md). - Icônes de sites : pas de favicon ni de capture d'écran tirés en direct des sites listés (fuite
de l'IP du lecteur vers des tiers, droits sur les images). Par défaut, un monogramme généré à
partir du nom, dans la teinte unique. Une image locale (
image=./logos/lorem.svg) reste possible sous la responsabilité de l'auteur.
D10 — Annuaires de personnes (entries: people)
Tranché (24/10, question 5) : pas de entries: people, pour la vie privée. Les règles
ci-dessous ne servent que si la question est rouverte un jour :
privacy: maskedpar défaut : ville sans rue,geoarrondi, e-mail et téléphone masqués (révélés au clic). Le masquage ne protège que contre la collecte automatique, la doc le dit ;- aucun JSON-LD
Personpar défaut,noindexen option ; - une page « retrait et rectification » fournie par le gabarit, avec le contact de l'éditeur ;
- un avertissement de lint pour toute entrée
person=trueavec une adresse de rue sansconsent=true.
D11 — Données structurées
Module JSON-LD partagé avec calendar (fonctions pures dans @recit/core, sous-chemin à nommer
avec P10, par exemple ./jsonld), pour qu'un Place soit émis de la même façon dans un événement
et dans une fiche d'annuaire. Glossaire → DefinedTermSet ; annuaire → ItemList. pages: true
rend l'annuaire éligible au carrousel en bêta, sans le promettre. Le glossaire part aussi
naturellement dans llms.txt.
D12 — Outillage et MCP
| Commande ou outil | Rôle |
|---|---|
recit convert places.csv --to directory | import CSV / JSON via @recit/data, avec une table columns: explicite (name: Nom, url: Site). Une colonne non mappée est ignorée avec un avertissement, jamais devinée |
recit convert places.md --to csv | export tabulaire, correspondance fermée sur les champs du schéma |
recit validate --links | vérification des liens (opt-in, réseau). Voir D13. Plus tard |
recit geocode places.md | commande unique de géocodage de Récit (aussi pour les cartes du plan 13) : ponctuelle, explicite, écrit geo dans le compagnon JSON sans réécrire la source (règle de 09), avec cache. Jamais au build. Fournisseur configurable, voir §5. Pas en v1 (tranché le 24/10) : plus tard |
MCP lookup_term(id | text, locale) | lecture seule : définition courte et longue |
MCP search_directory(query, filters) | lecture seule : entrées filtrées |
MCP describe_format("glossary" | "directory") | déjà prévu en 09 §7 |
Les deux outils MCP servent aussi le plan « scénariste » (ai.recit.dev) : le lexique d'un univers et
la liste de ses lieux deviennent des sources que l'agent consulte au lieu d'inventer. S'il faut un
skill produit pour le glossaire, il est généré par la commande unique recit skills export du plan
14 (IA3.3) depuis recit://guide/glossary, sans second générateur.
Import CSV, validate --links, geocode et JSON-LD (D11) sont plus tard : aucun n'entre dans
le lot P9 ni dans la première version de P14. Il n'y a pas d'import MkDocs (question 8).
D13 — Vérification des liens
recit validate --links (on ajoute une option à une commande existante plutôt qu'un nouveau verbe)
:
fetchnatif de Node, HEAD puis repli sur GET, concurrence bornée, pause par hôte, retries avec backoff, respect deRetry-After, User-Agent identifiable ;- cache local
.recit/cache/links.jsonavec âge maximal configurable ; - résultat écrit dans le compagnon JSON (
linkStatus,checkedAt,finalUrl). Une redirection vers un autre domaine est signalée (domaine expiré racheté, contenu changé) ; - par défaut : avertissement, le build ne casse pas.
--strictpour la CI de l'auteur ; - un lien en échec s'affiche avec un badge « à vérifier », sans être retiré ;
- les tests et e2e de Récit n'utilisent que
example.com,example.org,example.netet un serveur local : aucun réseau en CI.
4. Packages, site, exemples et tests
4.1 Packages
| Package | Groupe | Statut | Contenu | Dépendances notables |
|---|---|---|---|---|
@recit/core | fondations | ✏️ P9 | ./schema : Entry, GlossaryEntry, DirectoryEntry, Place, GlossarySource, DirectorySource, TermsConfig ; nom term dans le registre des directives. Analyseur {k=v} partagé avec roadmap. JSON-LD partagé (./jsonld, P10) | remark-directive, unist-util-visit (déjà là) |
@recit/components | fondations | ✏️ P14 | ./terms : plugin remark du lien automatique et de :term (DI1) ; Term (lien + carte au survol et au focus, P14) | @radix-ui/react-hover-card à la version déjà utilisée (1.1.23), passée au catalogue pnpm |
@recit/directory | formats | 📅 P9 | parseurs glossary et directory, vues, composants (§4.2), émission JSON-LD, pages par entrée, rétroréférences | @recit/core, @recit/components, @recit/data (import). Pas @recit/paged : le PDF passe par la CLI |
@recit/cli | distribution | plus tard | convert (csv), validate --links, geocode (après la v1) | aucune nouvelle |
@recit/mcp | distribution | ✏️ P14 | lookup_term, search_directory | aucune nouvelle |
@recit/site-stories | intégrations | ✏️ P14 | lien automatique des termes dans les pages du framework de sites, StoryEmbed pour les deux formats | aucune nouvelle |
@recit/site-kit | outils | ✏️ P14 | entrée directory dans SITES | aucune nouvelle |
Tags nx : @recit/directory porte scope:format et ne dépend que de fondations. Aucun package
publié n'importe @recit/site. Aucun patch du cœur du framework de sites.
Exception à 03-packaging.md §1 (« un package par format ») : @recit/directory déclare deux
formats, glossary et directory, parce qu'ils partagent tout le modèle (D1). Elle est à reporter
dans 03 §1 comme règle générale : « un package peut déclarer plusieurs formats s'ils partagent leur
modèle ».
Écartés : remark-abbr (cassé avec micromark), remark-deflist et mdast-util-definition-list (doublons fragiles), fuse.js et minisearch (Pagefind + filtre client suffisent), lychee (seconde stack), toute bibliothèque de géocodage, tout service de favicons.
4.2 Composants
Glossary (racine, choix de la vue), AlphabetRail, GlossaryEntry (nom, abréviation, définition,
renvois, source), UsedIn (rétroréférences), Directory (racine), EntryCard, EntryList,
EntryTable, DirectoryFilters (catégorie, tag, ville, lettre), EntryPage, LinkStatusBadge,
Monogram, et Term (dans @recit/components). Tous utilisables en MDX
(<StoryEmbed slug="places" format="directory" view="table" />).
4.3 Site
| Domaine | Rôle | Dossier | Contenu |
|---|---|---|---|
| directory.recit.dev | formats directory et glossary | sites/directory | hero = le glossaire de Récit (Story, Scene, sidecar, bridge, directive…) avec ses termes liés dans un paragraphe de démo, puis un annuaire Lorem en fiches, tableau et carte. Galerie, gabarits, démo bilingue (tri « É » sous « E ») |
| Alias | Cible | Pourquoi |
|---|---|---|
glossary.recit.dev | directory.recit.dev/glossary | second format du site, comme video → motion/video |
glossaire.recit.dev | directory.recit.dev/fr/glossary | mot du backlog |
annuaire.recit.dev | directory.recit.dev/fr | mot du backlog |
Recommandation : un seul site, directory.recit.dev, avec deux sections. Règle commune de
07-domaines.md §2 (proposée en 06, « Backlog ») : le domaine principal porte le nom anglais, et
les mots du backlog (glossaire., annuaire.) ainsi que le nom technique deviennent des alias 301.
Tranché le 24/10 (question 1) : un seul site avec alias.
- Vercel : projet
recit-directory, racinesites/directory, buildnx run directory:build, statique seulement. Aucun appel réseau au build (la vérification des liens n'y tourne pas). - Port local : 3012, d'après la table d'attribution unique de
07-domaines.md§4 (components 3010, form 3011, directory 3012, maps 3013, ai 3014). - Composants site-kit (
07§4) :FormatHero(glossaire jouable : survol, focus, Échap, avec la source dépliable),SourceToggle(rendu / Markdown / JSON),LocaleDemo(en / fr),FormatGallery,TemplateCard(npm create recit -- --template glossary-lorem,directory-links,directory-places),ComponentShowcase(Term,AlphabetRail,EntryCard,DirectoryFilters),ExportPanel(composant unique de@recit/site-kit, qui litexports.json: PDF, CSV, JSON, JSON-LD, chacun une variantekinddeExportManifestSchemade@recit/core, ajoutée dans la phase qui le produit),BridgeCard(glossaire → annexe de livre, annuaire → calendrier et stories),DocsLinks(versdocs.recit.dev/glossary/…et/directory/…),FormatFooter. - La doc reste sur docs.recit.dev (règle anti-doublon) : Formats › Glossary et Formats › Directory,
plus
docs.recit.dev/components/termpour:term, dans la section unique des blocs transverses. - Marque : icônes
brand/formats/fmt-glossary-*etfmt-directory-*dans la teinte unique Q07.
4.4 Exemples et e2e
| Exemple | Contenu |
|---|---|
examples/glossary-lorem | glossaire Lorem en/fr, deux sections, alias, renvois, plus un deck et une page MDX qui utilisent les termes |
examples/directory-links | une liste « awesome » Lorem brute, sans accolades, lue telle quelle en entries: links |
examples/directory-places | lieux Lorem en/fr (adresses fictives, example.*), compagnon JSON, import CSV, vue tableau et fiches |
Paquets recit-example-glossary-lorem, recit-example-directory-links,
recit-example-directory-places. Lorem Ipsum partout, aucune personne réelle.
e2e (e2e/glossary.e2e.ts, e2e/directory.e2e.ts), avec captures en clair, sombre et mobile :
- rail A–Z et tri
fr(« É » sous « E ») ; - terme lié dans un deck et une page de doc, non lié dans un bloc de code, un titre et un lien ;
- carte au survol ouverte au clavier (focus), fermée par Échap, et lien qui marche sans JavaScript ;
- filtres dans l'URL, retour arrière du navigateur ;
- page d'entrée et ancres ;
- JSON-LD présent et valide contre le schéma attendu (test unitaire sur la sortie) ;
axesans violation ;validate --linkscontre un serveur local qui répond 200, 301 vers un autre domaine, 404, 429.
5. Sécurité, vie privée, licences
5.1 Sécurité
- URL d'entrée limitées à
http:ethttps:(javascript:,data:refusés à la validation). - Liens externes avec
rel="noopener noreferrer", plussponsoredouugcselon l'entrée. - Le Markdown des fiches passe par le même pipeline (et la même sanitisation) que le reste de Récit : une entrée importée d'un CSV n'injecte pas de HTML brut.
- Aucun secret n'est requis. Le géocodage par un fournisseur à clé (si l'auteur en choisit un) lit
la clé dans l'environnement de la commande, jamais dans
recit.config.tsni dans le bundle.
5.2 Vie privée
- Aucune requête vers un tiers au chargement de la page : pas de favicons distants, pas de tuiles par défaut (la vue carte suit les règles du plan cartes), pas de géocodage au rendu.
- La vérification des liens et le géocodage ne tournent que sur la machine de l'auteur, sur commande.
- Pas de
entries: people(D10, tranché le 24/10). - Le gabarit d'annuaire fournit un champ
contactde l'éditeur et une page « retrait et rectification », même pourplacesetorganizations: une fiche d'entreprise individuelle peut identifier une personne.
5.3 Licences des contenus
- Une définition reprise d'ailleurs porte
source=etlicense=, affichés en note. Une source sous CC BY-SA (cas de Wikipédia) impose l'attribution et le partage dans les mêmes conditions : la doc le dit, et le lint avertit silicensecontientSAsanssource. - Un annuaire importé de données ouvertes déclare la source et la licence du jeu de données dans le
frontmatter (
sources: { places: { file, license, url } }). Lint : erreur en mode strict si la licence manque. Cas à documenter : des données OSM (ODbL) importées font de l'annuaire une base dérivée, soumise au partage à l'identique. - Le géocodage via Nominatim (si l'auteur le choisit) impose l'attribution OSM (ODbL) sur le site
publié, ajoutée automatiquement par le gabarit quand le compagnon contient des
geoissus de Nominatim.
5.4 Lignes NOTICE.md à prévoir
| Où dans NOTICE.md | Ligne |
|---|---|
| « What comes from where » | étendre formats/{deck,report,toon,book,roadmap,calendar,stories,paged} à …,directory} |
| « What comes from where » | sites/directory et examples/{glossary,directory}-* dans la ligne des sites et exemples Récit (à créer si elle n'existe pas) |
| « Third-party assets » | aucune : aucune nouvelle dépendance, aucun asset tiers. @radix-ui/react-hover-card est MIT et déjà présent |
| « Third-party assets » (si D12) | « OpenStreetMap data via Nominatim (recit geocode, opt-in) — ODbL, attribution required » si le géocodage Nominatim est livré. Ligne unique pour Récit : le plan 13 n'en ajoute pas |
| mention d'inspiration | « Glossary auto-linking rules inspired by docusaurus-plugin-glossary and mkdocs-ezglossary (ideas only, no code) », si on veut créditer les idées |
6. Phases
Lots DI0 à DI7, sous-tâches DIn.m (convention de 06, « Backlog »). Placement accepté le 24/10 dans
06-roadmap.md (question 3) :
| Lot | Phase |
|---|---|
DI0, plus la part entries: links de DI4 et une vue A–Z minimale de DI3 | lot avancé de P9 |
Place (DI0.1) | avec calendar, P10 |
DI1 (:term, puis lien automatique first en option), DI2, reste de DI3 et DI4, DI5, DI7 | P14 Directory |
| DI6, JSON-LD (DI3.4, DI4.3) | plus tard |
Le lot P9 vient avant la roadmap : l'analyseur {k=v} est créé ici (DI0.2), puis réutilisé par
le format roadmap (RM1 à RM5).
DI0 — Schémas (S)
- DI0.1 —
Entry,GlossaryEntry,DirectoryEntry,TermsConfigdans@recit/core/schema.glossaryetdirectoryajoutés àStoryFormatet àStorySourceSchema, comme ajout compatible en v1 (règle de versionnement de 06, « Backlog »).Placearrive aveccalendar(P10). - DI0.2 — Analyseur
{k=v}en fin de titre ou d'item, créé ici et réutilisé parroadmapen P9 (un seul module, testé une fois). - DI0.4 — Lot P9 : une liste « awesome » brute lue en
entries: links(vuescardsetlist) et une vue A–Z minimale du glossaire, dans@recit/directory. - DI0.3 — JSON Schema générés (
glossary.json,directory.json), ajoutés à la publicationrecit.dev/schema/v1.
Sortie : une liste « awesome » Lorem et un glossaire Lorem passent recit validate, avec
erreurs localisées (ligne et chemin JSON) sur des fixtures fautives ; examples/directory-links
s'affiche en fiches et en liste ; le glossaire Lorem s'affiche en A–Z.
DI1 — Termes transverses (M), P14
:term explicite fait partie de DI1 dans tous les cas. Le lien automatique (first, all) est une
option, livrée ensuite ; le défaut v1 reste none (question 2, tranchée).
- DI1.1 —
@recit/components/terms: automate de correspondance (alias le plus long, frontières Unicode, sans casse), plugin remark,:term[…]{id}et{off}, modesfirst/all/none. - DI1.2 — Section
termsderecit.config.tset surcharge par frontmatter. - DI1.3 —
Termdans@recit/components(lien réel + carte au survol et au focus). - DI1.4 — Branchement dans
@recit/site-stories(pages de doc) et dans@recit/deck. - Tests : zones ignorées (code, titres, liens, MDX, notes), collisions d'alias, langues, poids (seules les définitions utilisées sont embarquées).
Sortie : un terme est lié dans un deck et une page de doc, ignoré dans le code et les titres, la carte s'ouvre au clavier, et le lien marche sans JavaScript.
DI2 — Le glossaire de Récit (S), P14
- DI2.1 — Rédiger le glossaire de Récit (en, fr) avec les mots du modèle et de la syntaxe
(
Story,Scene,stories, sidecar, bridge, directive, format…). - DI2.2 — Activer
termssursites/docs.
Sortie : docs.recit.dev lie ses termes, et la confusion Story / stories notée en 08 §9 a
une définition à un seul endroit.
DI3 — Format glossary (M), P14 (vue A–Z minimale en lot P9)
- DI3.1 — Parseur (sections, H2, définition courte et longue, renvois, source).
- DI3.2 — Vues
azetsections,AlphabetRailavecIntl.Collator, ancres, pages par entrée. - DI3.3 — Rétroréférences « Used in » calculées au build sur tout le projet.
- DI3.4 — (plus tard) JSON-LD
DefinedTermSet(module partagé aveccalendar). - Tests : goldens des deux vues en en/fr, tri, renvois inconnus en erreur.
Sortie : examples/glossary-lorem s'affiche en A–Z en en et fr, avec rétroréférences.
DI4 — Format directory (M), P14 (entries: links en lot P9)
- DI4.1 — Parseur compatible « awesome » (H2, items,
{…},:::entry),entrieslinks,places,organizations. - DI4.2 — Vues
cards,list,table, facettes dans l'URL, filtre client,Monogram, mentionsponsored. - DI4.3 — Pages par entrée (
pages: true, opt-in ; défautfalse). JSON-LDItemList: plus tard. - DI4.4 — Compagnon JSON (statut des liens,
geo).
Sortie : examples/directory-links (liste brute) et examples/directory-places (en/fr)
s'affichent dans les trois vues, avec filtres partageables par URL.
DI5 — Ponts et exports (M), P14
- DI5.1 —
::glossaryet::directorydans le deck,--per entry/--per sectionpourrecit convert --to deck|stories|motion. - DI5.2 — Annexe glossaire dans
book(back: [glossary]) etreport, avec renvois de pages calculés à l'impression (par la CLI et@recit/paged, moteurpagedjs;@recit/directoryn'importe pas@recit/paged). - DI5.3 — L'annuaire comme source
@recit/data(:value … agg=count). - DI5.4 —
location=@iddanscalendar. - DI5.5 —
recit export --to pdfpour les deux formats.
Sortie : la même source de glossaire produit une page A–Z, une annexe de livre PDF, un deck d'une slide par terme et une story d'une carte par terme. Chaque pont a un test d'équivalence.
DI6 — Outillage (S), plus tard
- DI6.1 —
recit convertCSV ↔ directory (tablecolumns:explicite). Pas d'import MkDocs. - DI6.2 —
recit validate --links(cache,Retry-After, redirections inter-domaines). - DI6.3 — après la v1 :
recit geocode(opt-in, fournisseur configurable, cache, attribution), commande unique partagée avec les cartes (plan 13). - DI6.4 — MCP
lookup_term,search_directory, etdescribe_formatpour les deux formats.
Sortie : un CSV Lorem devient un annuaire puis revient en CSV sans perte, et validate --links
passe contre le serveur local de test sur les quatre cas (200, 301 inter-domaine, 404, 429).
DI7 — Site et doc (S), P14
- DI7.1 —
sites/directory(directory.recit.dev), projet Vercelrecit-directory, aliasglossary,glossaireetannuaire, entréeSITES, port 3012. - DI7.2 —
sites/docs› Formats › Glossary, Formats › Directory, Components › Term. - DI7.3 — Icônes
fmt-glossaryetfmt-directory, gabaritscreate-recit. - Tests : e2e de §4.4 avec captures,
axesans violation,i18n checkvert sur les exemples.
Sortie : le site est en ligne en en et fr, les trois exemples sont publiés, la version alpha correspondante est publiée après validation explicite.
Plus tard
-
Écarté le 24/10 (question 5, vie privée).entries: peopleavec les règles de D10. - Vue
mapquand le package de cartes existe. - Proposition d'entrée par formulaire (plan formulaires) : le formulaire produit une entrée
valide contre
DirectoryEntry, relue par l'auteur avant publication.
7. Risques
| Risque | Parade |
|---|---|
| Faux positifs du lien automatique sur des mots courants (« set », « map ») | mode first par scène, alias explicites, :term{off}, opt-in par déclaration de terms.sources, avertissement sur les termes d'un seul mot très courts |
| Lien automatique lent sur un gros projet | automate construit une fois par build et par langue, tests de charge sur une fixture générée (mesure avant tout chiffre publié) |
| Poids de page si tout le glossaire est embarqué | seules les définitions courtes des termes présents dans la page |
| Deux mécanismes de découpage des scènes | décision D2 : --- reste le seul séparateur, l'entrée est une sous-unité |
Doublon d'analyseur {k=v} avec roadmap | un seul module dans @recit/core (DI0.2) |
| Confusion glossaire / index de livre | deux notions distinctes, back: [glossary, index], documentées côte à côte |
| Liens externes morts ou rachetés | validate --links, détection des redirections inter-domaines, badge « à vérifier » |
| CI instable à cause du réseau | aucun réseau en CI, serveur local de test, --strict réservé à l'auteur |
| Annuaire qui publie des données personnelles | pas de entries: people ; masquage, page de retrait et lint si la question est rouverte |
| Données ou définitions sous licence virale (ODbL, CC BY-SA) | source/license par entrée et par jeu de données, lint, doc |
| Adresse libre mal interprétée | adresse structurée obligatoire pour le JSON-LD, aucune analyse de chaîne |
| Promesse SEO non tenue (carrousel en bêta, DefinedTerm sans rendu Google) | aucune promesse dans la doc ni sur le site, seulement « balisage émis » |
| Encore deux formats en parallèle d'une roadmap déjà longue | seul un petit lot en P9 avant la 1.0 ; le reste en P14 Directory |
8. Questions ouvertes pour l'utilisateur
Les questions communes au backlog (domaines en français, « Pet », « le skill », « le projet basic »,
le brocoli, l'exception lebrocoli.fr) sont regroupées une seule fois dans 06-roadmap.md, «
Questions ouvertes ». Un annuaire de lieux pour le gabarit lebrocoli.fr serait une section
optionnelle du dossier unique examples/scenario-lebrocoli (plan 14), avec des lieux réels ou
fictifs selon la réponse de l'utilisateur (06, « À l'utilisateur »). Les questions ci-dessous ont
été tranchées le 24/10/2026, sans l'utilisateur et à sa demande (décisions réversibles), sauf la 4 :
Domaine : un seul siteTranché (24/10) : un seul site, avec alias.directory.recit.dev, avecglossary,glossaireetannuaireen 301, ou deux sites ?Lien automatique : défaut v1Tranché (24/10) : liens automatiques désactivés par défaut en v1, modenone, puisfirstpar scène en option ?firsten option ensuite.Place dans la roadmap : lot avancé en P9, puis P14 Directory après la 1.0 : d'accord ?Tranché (24/10) : oui (ordre du backlog accepté, 06 question 14).- Glossaire et annuaire sont-ils bien la même idée pour toi (une collection d'entrées, deux lectures), ou pensais-tu à deux outils séparés ? (Le choix d'un seul site avec alias va dans le sens d'une seule idée.)
Annuaire de personnes (Tranché (24/10) : pas deentries: people) : hors V1, ou un besoin réel dès le départ ?entries: people(vie privée).Pages par entrée (Tranché (24/10) : pas de page par entrée par défaut (pages: true) activées par défaut, ou une seule page par défaut ?pages: false).Géocodage : fournir plus tardTranché (24/10) : coordonnées écrites par l'auteur, pas de commande de géocodage en v1. Vaut aussi pour les cartes (plan 13).recit geocode, ou s'en tenir aux coordonnées écrites par l'auteur ?Import MkDocs (Tranché (24/10) : pas d'import MkDocs.*[HTML]: …) : utile, ou à ignorer ?
9. Impact sur le reste du plan (à reporter, non fait ici)
| Fichier | Ce qui change |
|---|---|
02-architecture.md | StoryFormat gagne glossary et directory. Section terms dans la config (§5). :term dans la syntaxe commune (§3) |
03-packaging.md | @recit/directory 📅 dans le tableau des packages, nom npm à réserver. Extensions de @recit/core (schémas, nom term, ./jsonld) et de @recit/components (./terms, Term) |
06-roadmap.md | fait : lot P9 et P14 Directory dans « Backlog : plans 10 à 14 ». Décision datée du 24/10 ajoutée (question 3) |
07-domaines.md | fait : directory.recit.dev, alias glossary, glossaire, annuaire, projet Vercel recit-directory, port 3012 (et components = 3010) |
08-formats.md | lignes dans la vue d'ensemble et les ponts (§0), :term dans les composants transverses (§7), back: [glossary] dans book (§3), risque Story/stories (§9) traité par DI2. Corriger « neuf formats » |
09-langues-et-json.md | lignes glossary et directory en §2.3 et §5 (texte en D6 et D7), avec l'exception au repli de D7 citée en §3 et §5 |
08-formats.md §5 | forme de location : chaîne libre, @id d'annuaire ou Place inline, une seule résolution (D6) |
NOTICE.md | lignes du §5.4 |
brand/ | icônes fmt-glossary et fmt-directory |