Nouveaux blocs et outils

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 confusion Story / 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 format roadmap.
  • 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

OutilSyntaxeLien automatiqueCe qu'on retient
Material for MkDocs, abbr + auto_append*[HTML]: Hyper Text Markup Language dans un fichier ajouté à chaque pageoui, toutes les occurrencesun fichier de termes unique pour tout le projet
mkdocs-ezglossaryliste de définitions section:term, référence <section:term>non, expliciteplusieurs glossaires, page récapitulative avec rétroréférences (« où ce terme est utilisé »), sortie PDF
docusaurus-plugin-glossaryJSON de termes + <GlossaryTerm term>oui, plugin remark sans casse, frontières de motle lien automatique ignore code, liens et composants MDX
GRNET / eSSIF-Lab terminology%%term|term%%, une page par termenoncontre-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]: descouicassé avec micromark d'après la liste officielle des plugins remark : à ne pas adopter
remark-deflist / mdast-util-definition-listTerme puis : définition—deux paquets aux nœuds incompatibles, maintenance incertaine : une dépendance de plus, écartée
awesome-lists + awesome-lintH2 = 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é)

BesoinDéjà présentOù
Directives :term[…]{…}, :::entryremark-directive 3.0.1packages/core/package.json
Parcours de l'arbre Markdownunist-util-visit 5.1.0packages/core/package.json
Attributs {k=v} en fin d'itemconvention spécifiée pour roadmap08-formats.md §4
Carte au survol accessible@radix-ui/react-hover-card 1.1.23 (et tooltip, popover)packages/site/package.json
Recherche plein textepagefind 1.5.2, plugin search-pagefindpackages/site
Import CSV / JSON / YAMLprévu dans @recit/data (P6)08-formats.md §0
Index de fin de livre:index[term] et back: [notes, index] dans book08-formats.md §3
JSON-LD Event avec un lieuprévu dans calendar08-formats.md §5
llms.txtgénéré par le forkpackages/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 : DefinedTermSet qui contient des DefinedTerm (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éments LocalBusiness ou sous-type, Organization ou WebSite, avec PostalAddress, 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

3. Décisions proposées

D1 — Un package, deux formats, une fonction transverse

OptionPourContre
A. @recit/glossary et @recit/directorynoms évidentsmodèle, A–Z, filtres, recherche et JSON-LD en double : contraire à la stack unique
B. un seul format directory avec entries: glossaryun seul littéralle 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œurun modèle, deux lectures, rien en doubledeux littéraux de plus dans StoryFormat

Recommandation : C.

  • @recit/directory (packages/formats/directory) porte le modèle commun Entry et deux formats déclarés : format: glossary (A–Z et définitions) et format: 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œud term neutre), avec son rendu (Term, carte au survol). Le cœur ne garde que le nom term dans son registre et le schéma TermsConfig (règle B4 de 06-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, comme roadmap et calendar.

D2 — Une scène = une section (catégorie), l'entrée est l'unité du modèle

OptionEffet
une scène = une entrée, chaque H2 coupe une scèneajoute 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éelourd 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érieurmê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.

MarkdownMarkdown
--- title: Lorem Glossary format: glossary lang: en glossary: view: az # az | sections usedIn: true # "Used in" backreferences, computed at build time --- --- id=core # Core concepts ## Lorem {id=lorem aliases="lorems, lorem engine" see=ipsum tags=core} Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. The body accepts any Markdown: lists, code, `::image{src=./img/lorem.svg alt="Lorem diagram"}`, `:::callout`. ## Ipsum {id=ipsum aliases=ipsums abbr=IPS} Ut enim ad minim veniam, quis nostrud exercitation. --- id=data # Data ## Dolor set {id=dolor-set source="https://example.com/dolor" license=CC-BY-4.0} Duis aute irure dolor in reprehenderit in voluptate.
ÉlémentRôle
--- id=… + H1une 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 paragraphedéfinition courte, limitée par la règle de densité (avertissement au-delà du plafond de mots du thème)
reste du blocdé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, un see vers un id inconnu : 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) :

TypeScriptTypeScript
export default defineConfig({ terms: { sources: ["./glossary/glossary.md"], autolink: "none", // none | first | all page: "/glossary", // cible des liens }, });

Dans n'importe quelle story (deck, docs, report, book, stories) :

MarkdownMarkdown
--- title: Lorem Talk format: deck terms: autolink: none # refus pour cette story --- The :term[lorem engine]{id=lorem} reads a dolor set. Lorem is linked only once per scene. Explicit opt-out for one word: :term[Lorem]{off}.
ÉlémentRô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, :term existants, attributs, composants MDX, notes présentateur ;
  • par langue : une story en fr est liée avec les noms et alias de glossary.fr.md (§3, D7) ;
  • :term{id=…} vers un id inconnu : 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.

MarkdownMarkdown
--- title: Lorem Places format: directory lang: en entries: places # links | places | organizations (no people, see D10) view: cards # cards | list | table | map pages: false # default; true : une page par entrée (opt-in) facets: [category, tags, city] --- --- id=studios ## Studios - [Lorem Studio](https://example.com) — Dolor sit amet. {id=lorem-studio tags=design,print street="1 Lorem Street" city=Paris postcode=75000 country=FR geo=48.8566,2.3522} - [Ipsum Lab](https://example.org) — Consectetur adipiscing. {id=ipsum-lab tags=video city=Lyon country=FR} --- id=venues ## Venues - [Dolor Hall](https://example.net) — Sed do eiusmod. {id=dolor-hall tags=music city=Paris country=FR sponsored} :::entry{id=dolor-hall} Opening hours, access, accessibility notes. Any Markdown, including `::image{src=./img/hall.svg alt="Dolor Hall facade"}`. :::

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émentRô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.jsonchamps 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, country sont des champs distincts. Une chaîne libre address="…" est acceptée pour l'affichage seulement, et le JSON-LD PostalAddress n'est émis qu'à partir des champs structurés. Découper une adresse libre serait une devinette ;
  • geo hors de [-90, 90] / [-180, 180], country hors ISO, URL qui n'est pas http(s) : erreur avec numéro de ligne ;
  • id en double, :::entry vers un id inconnu : erreur ;
  • entrée sponsored : affichée avec une mention visible et rel="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.

TypeScriptTypeScript
// The single Place of Récit, owned by this plan. Calendar (P10) ships it; maps (plan 13) imports it. const Place = z.object({ name: z.optional(Localized(z.string())), address: z.optional(z.string()), // display only: never parsed, never used for JSON-LD street: z.optional(z.string()), city: z.optional(z.string()), postcode: z.optional(z.string()), region: z.optional(z.string()), country: z.optional(z.string().check(isoAlpha2)), geo: z.optional(z.tuple([z.number().check(lat), z.number().check(lon)])), }); const Entry = z.object({ id: z.string(), name: Localized(z.string()), aliases: z.optional(Localized(z.array(z.string()))), summary: z.optional(Localized(z.string())), // short definition / one-line description body: z.optional(Localized(z.string())), // Markdown tags: z.optional(z.array(z.string())), see: z.optional(z.array(z.string())), source: z.optional(z.object({ url: z.url(), license: z.optional(z.string()) })), }); // BibleEntry (plan 14) extends Entry too: one model of "named entries" in Récit. const GlossaryEntry = Entry.extend({ abbr: z.optional(z.string()), termCode: z.optional(z.string()) }); const DirectoryEntry = Entry.extend({ url: z.optional(z.url({ protocol: /^https?$/ })), email: z.optional(z.email()), phone: z.optional(z.string()), place: z.optional(Place), sameAs: z.optional(z.array(z.url())), sponsored: z.optional(z.boolean()), });

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 :

FormatUne scène JSON contientExemple de champ localisable
glossarysection : title, entries[] {id, name, aliases, summary, body, see, tags, abbr, source}entries[].name, entries[].aliases, entries[].summary
directorycaté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

DomaineRègle (ligne à ajouter à 09-langues-et-json.md §5)
glossaryglossary.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
directorycaté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.

VersGlossaireAnnuairePhase
docstermes liés dans toutes les pages, <StoryEmbed format="glossary" view="az" />, page /glossary avec ancres<StoryEmbed format="directory" view="cards" />, filtres dans l'URLDI2, 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 fichesDI5
reportannexe « Glossary » générée avec les seuls termes utilisés dans le rapportl'annuaire est une source de @recit/data : :value{data=places agg=count where="category=studios"}. Les chiffres sont calculés, jamais écritsDI5
bookback: [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
motionun 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 jamaisaprès le plan cartes
toon, roadmaprien de promisrien 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), renvois see, rétroréférences « Used in » calculées au build à partir des nœuds term de tout le projet.
  • Annuaire : vues cards, list, table, map (optionnelle). Facettes dans l'URL (?category=studios&tag=design&city=Paris), comme les filtres de roadmap.
  • 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 Term reste léger et n'importe aucun module lourd (règle « Bundle Size » de CLAUDE.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: masked par défaut : ville sans rue, geo arrondi, 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 Person par défaut, noindex en 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=true avec une adresse de rue sans consent=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 outilRôle
recit convert places.csv --to directoryimport 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 csvexport tabulaire, correspondance fermée sur les champs du schéma
recit validate --linksvérification des liens (opt-in, réseau). Voir D13. Plus tard
recit geocode places.mdcommande 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) :

  • fetch natif de Node, HEAD puis repli sur GET, concurrence bornée, pause par hôte, retries avec backoff, respect de Retry-After, User-Agent identifiable ;
  • cache local .recit/cache/links.json avec â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. --strict pour 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.net et un serveur local : aucun réseau en CI.

4. Packages, site, exemples et tests

4.1 Packages

PackageGroupeStatutContenuDépendances notables
@recit/corefondations✏️ 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/componentsfondations✏️ 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/directoryformats📅 P9parseurs 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/clidistributionplus tardconvert (csv), validate --links, geocode (après la v1)aucune nouvelle
@recit/mcpdistribution✏️ P14lookup_term, search_directoryaucune nouvelle
@recit/site-storiesintégrations✏️ P14lien automatique des termes dans les pages du framework de sites, StoryEmbed pour les deux formatsaucune nouvelle
@recit/site-kitoutils✏️ P14entrée directory dans SITESaucune 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

DomaineRôleDossierContenu
directory.recit.devformats directory et glossarysites/directoryhero = 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 »)
AliasCiblePourquoi
glossary.recit.devdirectory.recit.dev/glossarysecond format du site, comme video → motion/video
glossaire.recit.devdirectory.recit.dev/fr/glossarymot du backlog
annuaire.recit.devdirectory.recit.dev/frmot 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, racine sites/directory, build nx 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 lit exports.json : PDF, CSV, JSON, JSON-LD, chacun une variante kind de ExportManifestSchema de @recit/core, ajoutée dans la phase qui le produit), BridgeCard (glossaire → annexe de livre, annuaire → calendrier et stories), DocsLinks (vers docs.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/term pour :term, dans la section unique des blocs transverses.
  • Marque : icônes brand/formats/fmt-glossary-* et fmt-directory-* dans la teinte unique Q07.

4.4 Exemples et e2e

ExempleContenu
examples/glossary-loremglossaire Lorem en/fr, deux sections, alias, renvois, plus un deck et une page MDX qui utilisent les termes
examples/directory-linksune liste « awesome » Lorem brute, sans accolades, lue telle quelle en entries: links
examples/directory-placeslieux 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) ;
  • axe sans violation ;
  • validate --links contre 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: et https: (javascript:, data: refusés à la validation).
  • Liens externes avec rel="noopener noreferrer", plus sponsored ou ugc selon 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.ts ni 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 contact de l'éditeur et une page « retrait et rectification », même pour places et organizations : une fiche d'entreprise individuelle peut identifier une personne.

5.3 Licences des contenus

  • Une définition reprise d'ailleurs porte source= et license=, 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 si license contient SA sans source.
  • 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 geo issus de Nominatim.

5.4 Lignes NOTICE.md à prévoir

Où dans NOTICE.mdLigne
« 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) :

LotPhase
DI0, plus la part entries: links de DI4 et une vue A–Z minimale de DI3lot avancé de P9
Place (DI0.1)avec calendar, P10
DI1 (:term, puis lien automatique first en option), DI2, reste de DI3 et DI4, DI5, DI7P14 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, TermsConfig dans @recit/core/schema. glossary et directory ajoutés à StoryFormat et à StorySourceSchema, comme ajout compatible en v1 (règle de versionnement de 06, « Backlog »). Place arrive avec calendar (P10).
  • DI0.2 — Analyseur {k=v} en fin de titre ou d'item, créé ici et réutilisé par roadmap en P9 (un seul module, testé une fois).
  • DI0.4 — Lot P9 : une liste « awesome » brute lue en entries: links (vues cards et list) 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 publication recit.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}, modes first / all / none.
  • DI1.2 — Section terms de recit.config.ts et surcharge par frontmatter.
  • DI1.3 — Term dans @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 terms sur sites/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 az et sections, AlphabetRail avec Intl.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é avec calendar).
  • 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.1 — Parseur compatible « awesome » (H2, items, {…}, :::entry), entries links, places, organizations.
  • DI4.2 — Vues cards, list, table, facettes dans l'URL, filtre client, Monogram, mention sponsored.
  • DI4.3 — Pages par entrée (pages: true, opt-in ; défaut false). JSON-LD ItemList : 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 — ::glossary et ::directory dans le deck, --per entry / --per section pour recit convert --to deck|stories|motion.
  • DI5.2 — Annexe glossaire dans book (back: [glossary]) et report, avec renvois de pages calculés à l'impression (par la CLI et @recit/paged, moteur pagedjs ; @recit/directory n'importe pas @recit/paged).
  • DI5.3 — L'annuaire comme source @recit/data (:value … agg=count).
  • DI5.4 — location=@id dans calendar.
  • DI5.5 — recit export --to pdf pour 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 convert CSV ↔ directory (table columns: 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, et describe_format pour 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 Vercel recit-directory, alias glossary, glossaire et annuaire, entrée SITES, port 3012.
  • DI7.2 — sites/docs › Formats › Glossary, Formats › Directory, Components › Term.
  • DI7.3 — Icônes fmt-glossary et fmt-directory, gabarits create-recit.
  • Tests : e2e de §4.4 avec captures, axe sans violation, i18n check vert 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

  • entries: people avec les règles de D10. Écarté le 24/10 (question 5, vie privée).
  • Vue map quand 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

RisqueParade
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 projetautomate 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ènesdécision D2 : --- reste le seul séparateur, l'entrée est une sous-unité
Doublon d'analyseur {k=v} avec roadmapun seul module dans @recit/core (DI0.2)
Confusion glossaire / index de livredeux notions distinctes, back: [glossary, index], documentées côte à côte
Liens externes morts ou rachetésvalidate --links, détection des redirections inter-domaines, badge « à vérifier »
CI instable à cause du réseauaucun réseau en CI, serveur local de test, --strict réservé à l'auteur
Annuaire qui publie des données personnellespas 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éeadresse 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à longueseul 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 :

  1. Domaine : un seul site directory.recit.dev, avec glossary, glossaire et annuaire en 301, ou deux sites ? Tranché (24/10) : un seul site, avec alias.
  2. Lien automatique : défaut v1 none, puis first par scène en option ? Tranché (24/10) : liens automatiques désactivés par défaut en v1, mode first en option ensuite.
  3. 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).
  4. 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.)
  5. Annuaire de personnes (entries: people) : hors V1, ou un besoin réel dès le départ ? Tranché (24/10) : pas de entries: people (vie privée).
  6. Pages par entrée (pages: true) activées par défaut, ou une seule page par défaut ? Tranché (24/10) : pas de page par entrée par défaut (pages: false).
  7. Géocodage : fournir plus tard recit geocode, ou s'en tenir aux coordonnées écrites par l'auteur ? Tranché (24/10) : coordonnées écrites par l'auteur, pas de commande de géocodage en v1. Vaut aussi pour les cartes (plan 13).
  8. Import MkDocs (*[HTML]: …) : utile, ou à ignorer ? Tranché (24/10) : pas d'import MkDocs.

9. Impact sur le reste du plan (à reporter, non fait ici)

FichierCe qui change
02-architecture.mdStoryFormat 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.mdfait : lot P9 et P14 Directory dans « Backlog : plans 10 à 14 ». Décision datée du 24/10 ajoutée (question 3)
07-domaines.mdfait : directory.recit.dev, alias glossary, glossaire, annuaire, projet Vercel recit-directory, port 3012 (et components = 3010)
08-formats.mdlignes 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.mdlignes 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 §5forme de location : chaîne libre, @id d'annuaire ou Place inline, une seule résolution (D6)
NOTICE.mdlignes du §5.4
brand/icônes fmt-glossary et fmt-directory