note this well
notabene
Documentation complète
Guide
Qu'est-ce que notabene ?
Qu’est-ce que notabene ?
nota bene — la marque de marge qui signifie « à bien noter ».
notabene rend le Markdown/MDX de votre repo comme un site navigable avec des commentaires de revue directement sur la page, et fournit le protocole de revue humain↔agent qui transforme ces commentaires en éditions. Le viewer est le support — le protocole est le produit.
Le problème qu’il résout
Corriger ou rédiger de la doc avec un agent IA oblige à mettre chaque changement en mots : citer le passage, nommer la section — « revoir la formulation de la section 3 » — puis espérer que l’agent retrouve l’endroit exact dans la source. Passé deux ou trois changements, c’est un pavé d’instructions dans un seul champ de chat. L’instruction réelle a toujours été plus simple : ce passage — le changer comme ça.
notabene en fait l’interface. Sélectionnez le texte exact sur la page rendue — ou un diagramme ou une image en entier — et laissez un commentaire à cet endroit. Le commentaire ancré est l’instruction : situé, sans ambiguïté, rien à citer. L’agent lit les commentaires, édite la source fidèlement, marque chaque commentaire résolu et journalise ce qui a changé & pourquoi.
La boucle, en 30 secondes
npx notabene dev→ ouvrir le site, sélectionner du texte → laisser un commentaire (ou commenter une page, un diagramme ou une image en entier). Fils, résolution, mise en attente, une vue globale/comments.- Demander à l’agent : « traite les commentaires de la doc ».
- L’agent lit
.notabene/, édite les docs fidèlement, marque chaque commentaire résolu et ajoute une entrée de journal (quoi / pourquoi / quels commentaires). - Relire la trace sur
/journal— ou valider chaque diff vous-même en mode approve.
Tout est dans votre git
Commentaires et journal sont des fichiers JSON sous .notabene/ — pas de SaaS, pas
de base de données, pas de compte. Ils voyagent avec le repo, diffent dans les PR, et
sont lisibles par n’importe quel agent : le protocole de revue est file-I/O-first
(ni serveur, ni port, ni MCP requis).
Où aller ensuite
- Installation — le renderer npm, le plugin Claude Code, ou les deux.
- Votre première revue — du commentaire à l’édition journalisée.
- Configuration — le fichier de config unique, par l’exemple : espaces, branding, liens de navigation et pied de page, page d’accueil.
- Personnaliser le rendu — branding, tokens, polices, thème de code, feuille de style.
- La boucle de revue — auto ou approve (diffs avec humain dans la boucle).
- Rédiger la doc — la palette de rendu : GFM, Mermaid, code, liens.
- Doc multilingue — EN/FR/… avec URL propres et sélecteur.
- Export PDF — vues imprimables et PDF avec signets.
- Publier un site public — un site statique en lecture seule, lisible par les agents.
Plutôt les tableaux exhaustifs ? Direction l’espace Référence : la CLI, chaque clé de config, le frontmatter, le contrat du store et le modèle de sécurité.
Installation
Installation
notabene, c’est deux briques installables : le plugin Claude Code (mise en place clé en main + la boucle de revue) et le renderer (un paquet npm + une CLI). Elles s’installent indépendamment — le plugin n’a pas besoin du paquet npm : il récupère le renderer tout seul.
Le plugin Claude Code — mise en place + revue
Dans Claude Code :
/plugin marketplace add z29k/notabene
/plugin install notabene@z29k
C’est toute l’installation — aucun npm install requis. Le plugin récupère et
exécute lui-même une version épinglée du renderer via npx (le premier lancement la
télécharge, ~30 s) ; rien n’est scaffoldé dans votre repo, qui n’a même pas besoin d’un
package.json. Le seul prérequis est Node (voir prérequis plus bas).
Puis dites simplement « configure notabene » (repo vierge) ou « traite les commentaires de la doc » (déjà configuré) — la bonne skill se déclenche d’elle-même.
Vous préférez une installation manuelle ? Copiez packages/claude-plugin/skills/notabene/ dans
le .claude/skills/ de votre projet. Vous utilisez un tout autre agent ? Le plugin ne vous
sert à rien : notabene init écrit le même protocole dans <store>/protocol.md et le
référence depuis AGENTS.md (voir
l’utiliser depuis n’importe quel agent).
Le renderer — paquet npm (sans Claude)
Pour piloter la CLI vous-même — à la main, en CI, ou depuis un autre outil — installez le paquet npm :
npm install -D @z29k/notabene # or: pnpm add -D @z29k/notabene · bun add -d @z29k/notabene
npx notabene init # writes notabene.config.mjs + creates the .notabene store
npx notabene dev # → http://localhost:3009
Le paquet npm est scopé (
@z29k/notabene) ; la commande CLI qu’il installe est simplementnotabene, doncnpx notabene …fonctionne tel quel.
init est la seule chose qui touche votre repo — il écrit notabene.config.mjs et
crée le store .notabene/ (init --detect préremplit roots[] à partir des dossiers de
doc qu’il trouve). Le renderer lui-même s’exécute depuis le paquet : rien n’est
scaffoldé ni copié dans votre repo, et la mise à jour se résume à npm update.
La surface de commandes complète (build, pdf, status, migrate, comments, journal…) est dans la référence CLI.
Installer les deux ne pose aucun problème : le plugin exécute toujours sa propre
version épinglée du renderer (alignée sur la version du plugin), indépendante de celle
de votre package.json — les deux ne se marchent jamais dessus.
Prérequis
Les deux voies partagent les mêmes prérequis :
- Node ≥ 22.12 et
npxdans votre PATH (les deux sont fournis avec Node). - N’importe quel repo avec des fichiers Markdown ou MDX — voir MDX et CommonMark pour la façon dont les deux formats sont gérés.
Ensuite : votre première revue.
Votre première revue
Votre première revue
Vous avez installé le renderer et lancé npx notabene dev. Voici la
boucle complète, une fois, de bout en bout.
1 · Commenter la page rendue
Ouvrez http://localhost:3009, naviguez vers n’importe quelle page et sélectionnez un
passage — une barre d’action apparaît ; laissez votre commentaire là, directement. Vous
pouvez aussi :
- commenter une page entière (la zone de commentaire en bas de chaque page) ;
- commenter un diagramme ou une image en entier — survolez-le et utilisez le 💬 de la barre d’outils (le ⤢ juste à côté ouvre une lightbox pan/zoom) ;
- répondre dans les fils, placer un commentaire en mise en attente (⏸ — l’agent
le sautera), et tout voir, toutes pages confondues, sur
/comments.
Sur téléphone ou tablette, la même boucle fonctionne d’abord au tactile : la nav se replie dans un tiroir, les commentaires deviennent des bottom sheets, et vous pouvez sélectionner du texte et commenter au pouce. Tirez la poignée d’une feuille vers le haut pour l’agrandir, vers le bas pour la fermer.
2 · Confier les commentaires à votre agent
Dites-le à votre agent — avec le plugin Claude Code, c’est simplement :
traite les commentaires de la doc
L’agent lit le store .notabene/ directement (aucun serveur requis), localise chaque
passage commenté dans le fichier source, applique le retour fidèlement, marque le
commentaire résolu, et ajoute une entrée de journal reliant ce qui a changé à
pourquoi. Il vérifie ensuite : le build du renderer s’exécute toujours, plus les checks
que vous listez dans verify[]. Il ne commite jamais sans
demander.
3 · Relire la trace
/journal— chaque passe, avec quoi/pourquoi/quels commentaires, par page.- Chaque commentaire résolu pointe vers son entrée de journal.
Vous voulez valider chaque édition vous-même avant qu’elle compte comme résolue — avec le vrai diff git ? C’est le mode approve : voir la boucle de revue.
Travailler à plusieurs relecteurs
Les commentaires portent une identité (nom + email optionnel), définie par navigateur via la pastille 👤 de l’en-tête — les fils s’attribuent donc par personne, pas par machine. Le store, c’est un fichier JSON par commentaire, donc les branches parallèles fusionnent sans conflit.
Configuration
Configuration
notabene.config.mjs à la racine de votre repo est le seul câblage. Les chemins sont
relatifs au repo. notabene init scaffolde un template commenté ; notabene init --detect préremplit roots[] à partir des dossiers de doc qu’il trouve. Chaque clé est
optionnelle — cette page montre celles que vous toucherez vraiment ; la
référence de config les liste toutes.
// notabene.config.mjs
export default {
siteName: "My Project",
tagline: "docs",
locale: "en",
// Format d'entrée : "commonmark" (tolérant, zéro MDX) ou "mdx" (.mdx strict + .md tolérant).
format: "commonmark",
// Espaces de doc. `key` = slug d'URL + espace du store ; `path` = dossier relatif au repo.
roots: [
{ key: "docs", label: "Docs", path: "docs", exclude: [".notabene/**"] },
{ key: "adr", label: "Decisions", path: "docs/adr", description: "Architecture decision records" },
],
store: "docs/.notabene", // commentaires + journal (committez ce dossier)
review: "auto", // "approve" = vous validez chaque édition avec un diff
verify: [], // vos propres checks post-édition (le build du renderer s'exécute toujours)
};
Espaces (roots[])
Chaque entrée devient un espace : sa propre section dans la sidebar, sa propre carte
sur la page d’accueil, son propre préfixe dans les ids de commentaires. Un root imbriqué
(comme docs/adr ci-dessus) l’emporte sur son parent pour les pages qu’il contient — le
chemin le plus spécifique fait loi. label et description acceptent une map par locale
quand l’i18n est activée.
MDX et CommonMark/GFM
Le renderer choisit le processeur selon l’extension du fichier :
.md→ CommonMark/GFM, tolérant.<email@x>,Promise<T>,{var}, le HTML brut et les tableaux GFM se rendent tous sans crash..mdx→ MDX strict (JSX/expressions) — composants importables, mais les</{hors blocs de code doivent être échappés.
format: "mdx" (le défaut de la config) active les deux, mélangeables dans un même repo.
format: "commonmark" (ce que init scaffolde) supprime entièrement la dépendance MDX —
le point de départ sûr et le plus tolérant pour un repo en Markdown pur.
Branding
Pointez l’en-tête, l’onglet du navigateur et les cartes sociales vers vos propres assets — des fichiers relatifs au repo, servis par le renderer (rien à copier nulle part) :
branding: {
logo: "assets/logo.svg", // image de la topbar, à côté du nom du site
logoDark: "assets/logo-dark.svg", // variante sombre optionnelle (sinon logo partout)
favicon: "assets/favicon.svg", // .svg / .ico / .png — non défini → une marque par défaut intégrée
socialImage: "assets/og.png", // og:image / twitter:image des builds PUBLICS
},
socialImage requiert publish.site — les crawlers
exigent une URL absolue. Le favicon couvre aussi les vues print/PDF.
Liens de navigation
Une fois dans une page, plus rien ne ramène au repo, au produit ou aux releases. nav
ajoute ces liens sortants à trois endroits — une seule forme d’entrée partout :
nav: {
// Topbar, groupe de droite. Répliqué automatiquement dans le tiroir mobile.
header: [
{ label: "GitHub", href: "https://github.com/vous/repo", icon: "github", iconOnly: true },
{ label: { en: "Product", fr: "Produit" }, href: "https://example.com" },
],
// Un bloc titré sous l'arbre des espaces (le tiroir mobile l'affiche aussi).
sidebar: {
title: { en: "Resources", fr: "Ressources" },
links: [
{ label: "Releases", href: "https://github.com/vous/repo/releases", icon: "star" },
{ label: "npm", href: "https://www.npmjs.com/package/votre-pkg", icon: "npm" },
],
},
// Le pied de page du site. Rien de configuré → aucun élément de pied de page.
footer: {
links: [{ label: "Licence", href: "/reference/licence" }],
text: { en: "© 2026 vous — MIT", fr: "© 2026 vous — MIT" },
poweredBy: false,
},
},
| Champ | Signification |
|---|---|
label | Obligatoire. Une chaîne, ou une map { <locale>: string } comme roots[].label. Sert aussi de nom accessible quand iconOnly |
href | Obligatoire. Une URL https:///http:///mailto: (ouverte dans un nouvel onglet, rel="noopener"), ou un chemin du site /… (votre publish.base est appliqué pour vous) |
icon | Au choix : github, gitlab, npm, discord, slack, x, mastodon, rss, mail, book, home, star, download, external. Monochrome — l’icône suit la couleur du lien, donc votre thème |
iconOnly | Topbar uniquement : n’affiche que l’icône (le label devient son aria-label/infobulle). Ignoré ailleurs |
publish | false garde le lien hors des builds publics — même idée que roots[].publish |
- Tout est validé au chargement de la config : une clé inconnue, une icône inconnue,
un href en double ou une URL
javascript:lèvent une erreur immédiatement plutôt que d’expédier un lien cassé — ou piégé — dans un site publié. - Ces liens relèvent de l’identité, pas de l’outillage de revue : contrairement à Commentaires/Revue/Journal, ils apparaissent en dev et dans les builds publics. Ils n’apparaissent jamais dans les vues print/PDF et restent hors de l’index de recherche.
- Gardez trois ou quatre entrées en topbar —
iconOnlyexiste précisément parce que cette rangée est chargée. Les listes longues vont dans le bloc sidebar ou le pied de page.
Page d’accueil personnalisée
Par défaut, la page d’atterrissage (/) affiche le nom du site et une carte par espace.
Pointez home vers un fichier Markdown pour rendre votre propre accueil au-dessus de
ces cartes — le classique est une intro façon README écrite pour le site, avec des
liens relatifs qui deviennent des routes :
home: "docs/home.md",
- Pipeline complet : Mermaid, coloration du code, et liens inter-docs réécrits vers
les routes du site — liez directement dans vos espaces (
[install](./guide/install.md)). - À garder de préférence hors de vos espaces (un doc dédié) : dans un espace, il se rendrait aussi comme une page normale de cet espace.
- Avec l’i18n, passez une map par locale :
home: { en: "docs/home.md", fr: "docs/home.fr.md" }— la page d’atterrissage de chaque locale (/,/fr) rend son propre fichier. - La page d’accueil de ce site, c’est exactement ça — voir
docs/home.md.
Pied de page : lien d’édition & dernière mise à jour
Deux touches sans configuration sous chaque page :
- Mis à jour le — la date d’auteur git de la page (un seul
git logstreamé par build ; une datelastUpdateden frontmatter la remplace ; silencieusement absente hors d’un repo git). Les builds publics l’émettent aussi enarticle:modified_time. - Modifier cette page — posez
editPatternet chaque page pointe vers sa source :
editPattern: "https://github.com/vous/repo/edit/main/{path}",
Il n’est rendu que là où l’éditeur dans la page n’est pas disponible :
builds et sites publiés. Sous notabene dev, l’éditeur fait la même chose en mieux.
Édition dans la page
Sous notabene dev, un ✎ dans la marge ouvre n’importe quel bloc à l’édition directement
dans la page — voir Éditer dans la page. Actif par défaut et sans
configuration ; voici les réglages si besoin :
edit: {
enabled: true, // false masque entièrement l'éditeur
requireGit: true, // false autorise l'écriture d'un fichier non suivi par git
},
roots: [
{ key: "reference", path: "docs/reference", edit: false }, // espace en lecture seule
],
Labels & ordre de la sidebar
Par défaut, l’entrée d’une page dans la sidebar est son nom de fichier humanisé, et les pages sœurs se trient alphabétiquement. Surchargez l’un ou l’autre par page via le frontmatter — aucun préfixe numérique dans les noms de fichiers requis :
---
title: Internal network map # <title> de la page + fil d'Ariane (surcharge le H1)
sidebar:
label: Network map # texte de la sidebar (sinon title, sinon nom de fichier)
order: 9 # position parmi les pages sœurs (croissant)
---
ordertrie en croissant ; les entrées qui n’en ont pas continuent de se trier alphabétiquement, après celles qui sont ordonnées. Groupes et pages partagent un même ordre.- Un dossier est nommé et positionné par sa page d’atterrissage —
<folder>/index.md(oureadme.md) — dont le frontmattersidebars’applique à tout le groupe ; cette page apparaît comme une entrée Aperçu localisée (renommable viasidebar.indexLabel). - Ces labels se propagent aux fils d’Ariane et à l’export PDF.
La surface complète du frontmatter (y compris description et publish pour les
sites publics) est dans la
référence du frontmatter.
Personnaliser le rendu
Personnaliser le rendu
notabene s’exécute depuis le paquet — vous ne forkez jamais son UI. Tout ce qui suit passe par la config à la place :
- Branding — logo, favicon, image sociale.
- Tokens — surchargez les custom properties
--nb-*(ci-dessous). - Votre propre feuille de style — un fichier CSS chargé après les styles du renderer.
- Polices et images — un dossier de votre repo, servi pour cette feuille de style.
- Code et diagrammes — un thème Shiki pour les blocs de code ; Mermaid suit les tokens.
Les liens sortants (topbar, sidebar, pied de page) n’en font pas partie : ce sont des données du dépôt, pas de l’apparence — voir liens de navigation.
theme: {
// Surcharges rapides, aucun fichier requis. Une valeur simple s'applique aux DEUX schémas ;
// une paire light-dark() personnalise chacun : clair à gauche, sombre à droite.
tokens: { accent: "light-dark(#7c3aed, #b79bff)", radius: "4px" },
css: "docs/notabene-theme.css", // ou/et une feuille de style complète
},
Les deux ciblent le même contrat ; un nom de token mal orthographié lève une erreur au démarrage (jamais un no-op silencieux). Les styles du renderer vivent dans des cascade layers CSS, donc votre CSS hors layer gagne toujours — pas de guerre de spécificité, pas de loterie d’ordre.
Tout suit cette palette — le chrome, les blocs de code et les diagrammes — et le sélecteur de schéma de l’en-tête la bascule en direct, sans rebuild :
Diagrammes
Les diagrammes Mermaid suivent les tokens d’emblée : les nœuds sont
remplis en accent-soft et bordés d’accent, les libellés utilisent la couleur du
texte, les arêtes text-soft — et ils se re-rendent au changement de schéma, donc un
diagramme en mode sombre est un vrai diagramme sombre, pas une image inversée. Rien à
configurer.
Si un diagramme rend mieux avec la palette de Mermaid, désactivez :
theme: { mermaid: false }, // retour aux thèmes default/dark intégrés de Mermaid
Coloration syntaxique (theme.code)
Les blocs de code sont colorés au build : leurs couleurs sont donc figées — d’où le décalage habituel, un thème clair avec des blocs de code sombres. Nommez un thème Shiki et cela change :
theme: {
code: "github-light", // même thème dans les deux schémas
// ou un par schéma :
code: { light: "github-light", dark: "vesper" },
},
- Avec
code, les deux palettes voyagent en variables CSS et le sélecteur de schéma recolore le code instantanément — sans rebuild, sans clignotement. - Le thème de code prend alors aussi la main sur le fond du bloc (les tokens d’un
thème clair sur la dalle sombre par défaut seraient illisibles).
--nb-code-bgreste le fond pour quiconque ne définit pastheme.code. - Le PDF/print force le schéma clair : c’est donc votre thème de code clair qui atterrit sur le papier.
- Nom de thème inconnu → erreur au démarrage, comme pour chaque réglage de thème.
Polices et images (theme.assets)
Une feuille de style a généralement besoin de fichiers : une police web, un fond, une
texture. Pointez theme.assets vers un dossier de votre repo : il est servi au chemin
fixe /_nb/assets/… — en dev, dans les builds et dans les
artefacts publics, pour qu’un site publié reste autonome (pas de CDN) :
theme: { css: "docs/theme/site.css", assets: "docs/theme/assets" },
/* docs/theme/site.css */
@font-face {
font-family: "Inter";
src: url("./assets/fonts/Inter.woff2") format("woff2"); /* ← relatif, toujours */
}
:root { --nb-sans: "Inter", system-ui, sans-serif; }
- Écrivez les
url()en relatif, jamais en absolu depuis la racine. Les URL se résolvent contre la feuille servie (/_nb/theme.css), pas contre votre fichier source — donc./assets/…est la forme correcte, et elle absorbe gratuitement un sous-cheminbase(site projet GitHub Pages), là où/_nb/…casserait. - Déclarez un dossier dédié, pas
docs/: tout ce qui y est servable est émis, référencé ou non. - Seuls des assets sont servis — polices (
woff2,woff,ttf,otf), images (svg,png,jpg,webp,avif,gif,ico) etcss. Tout le reste (.md,.env, scripts), chaque fichier caché et tout lien symbolique pointant hors du dossier sont refusés.
Le contrat de tokens (--nb-*)
Ces custom properties sont la surface de theming publique — stable d’une version à
l’autre. Les défauts de couleur sont donnés sous leur paire light-dark(light, dark) :
| Token | Défaut (clair / sombre) | Rôle |
|---|---|---|
bg | #ffffff / #0e1116 | Fond de page |
bg-soft | #f6f7f9 / #151a21 | Panneaux, champs de saisie |
bg-elev | #ffffff / #161b22 | Surfaces élevées (topbar, popovers) |
border | #e4e7ec / #272e38 | Filets |
text | #1c2024 / #e7ebf0 | Premier plan principal |
text-soft | #5b6470 / #aab2bd | Premier plan secondaire |
text-faint | #8a929e / #768091 | Premier plan tertiaire |
accent | #2f6feb / #6ea0ff | Liens, focus, surlignages |
accent-soft | #e8f0ff / #182539 | Fonds d’accent |
ref / work | #2f6feb / #6ea0ff · #b5651d / #e0a060 | Pastilles d’espace dans les résultats de recherche |
code-bg | #0d1117 / #0b0e13 | Fond des blocs de code — le défaut, quand aucun theme.code n’est défini (les couleurs Shiki sont alors figées clair-sur-sombre, donc le code reste sombre dans les deux schémas) |
topbar-h / sidebar-w / toc-w / content-max | 52px / 290px / 320px / responsive | Mise en page (invariante au schéma) |
radius | 8px | Arrondi des coins |
sans / mono | piles système | Familles de polices |
Chaque token de couleur est une paire light-dark() — une seule déclaration couvre
les deux schémas, et le sélecteur clair/sombre de l’en-tête (auto / clair / sombre,
persisté par navigateur) les bascule tous d’un coup. Un thème qui change les couleurs
fait de même :
/* docs/notabene-theme.css */
:root {
--nb-accent: light-dark(#7c3aed, #b79bff);
--nb-accent-soft: light-dark(#f1e9ff, #241a3d);
}
Le sélecteur fonctionne via color-scheme + un attribut data-scheme que le renderer
pose sur <html> — un thème ne doit pas définir cet attribut (ni color-scheme sur
:root) ; surchargez des tokens, et le sélecteur continue de fonctionner sans effort.
light-dark() ne couvre que les couleurs ; pour le rare style par schéma hors couleur,
sélectionner sur :root[data-scheme="dark"] dans votre feuille de style est très
bien — c’est poser l’attribut qui est réservé.
Au-delà des tokens
Votre feuille de style peut aussi cibler un petit ensemble de points d’accroche
stables : .topbar, .brand, .sidebar, .prose (le contenu rendu), .home-cards,
.rail, plus les liens de navigation —
.nb-nav-link (tout lien sortant), .nb-sidebar-links (le bloc sous l’arbre des
espaces) et .site-footer. Tout le reste — et chaque variable CSS sans préfixe — est
interne et peut changer d’une version à l’autre.
Un thème style ces liens ; il n’en déclare jamais. Les entrées de nav, le texte
du pied de page et les assets de branding sont des données du dépôt (nav, branding au
premier niveau de la config), pas de l’apparence : une feuille de style installée ne doit
pas pouvoir injecter des liens sortants dans un site publié, ni porter des libellés dans
des langues qu’elle ne peut pas connaître.
Deux règles vous gardent en sécurité :
- Ne surchargez que les tokens
--nb-*et les points d’accroche ci-dessus. En particulier, ne touchez jamais aux variables sans préfixe : les vues print/PDF forcent une palette claire à travers elles, donc un thème limité aux tokens peut restyler tout le site sans jamais casser l’export PDF. - Vérifiez les deux schémas de couleur — le sélecteur de l’en-tête en fait un test à deux clics.
Les thèmes s’appliquent partout : dev, builds normaux, sites publics et les vues print (couleurs exceptées, à dessein).
La boucle de revue
La boucle de revue
La boucle est le produit : des commentaires en entrée, des éditions fidèles en sortie, le tout journalisé. Elle est conçue pour que n’importe quel agent puisse l’exécuter — le protocole est un fichier de skill en texte brut qui lit et écrit le store directement.
Comment l’agent travaille
Tout est découvert depuis notabene.config.mjs — rien de codé en dur, ni serveur ni
port requis :
- Lire les commentaires ouverts et non mis en attente depuis
<store>/(un fichier JSON par commentaire). - Localiser la page source via
roots[], puis résoudre l’ancre textuelle avec tolérance (l’ancre stocke le texte cité + le contexte environnant + le titre le plus proche). - Éditer les docs fidèlement — un commentaire est une décision de l’utilisateur.
- Marquer le commentaire résolu (ou
addresseden mode approve) et compléter le journal : une entrée par passe, un enregistrement de changement par page touchée, relié aux ids des commentaires. - Vérifier : le build du renderer tourne toujours, puis
notabene lint(les liens inter-docs validés contre les routes que le build vient d’émettre), puis vos checksverify[]. - Rendre compte et demander avant de committer — jamais de commit silencieux, jamais de suppression en masse.
Les commentaires qu’un relecteur met en attente (⏸) sont ignorés — ce sont vos travaux en cours.
Les étapes 4 et 5 ont des primitives CLI, pour qu’un agent n’édite jamais le JSON du store
à la main : notabene comments done <id…> --note … --journal <entryId> choisit le bon
statut selon votre mode review, et notabene comments verify audite ce qu’il a écrit
— statuts, liens commentaire↔journal dans les deux sens, disposition des fichiers. Il sort
en code non nul sur un vrai problème : c’est donc aussi un garde-fou de CI sur le store que
vos agents committent.
Mode approve : un humain valide chaque édition
Par défaut (review: "auto"), l’agent résout les commentaires directement. Passez
review: "approve" pour un flux avec humain dans la boucle :
- l’agent édite et marque chaque commentaire
addressedau lieu de le résoudre ; - vous validez sur
/review(ou via le filtre À valider de/comments) ; - vous voyez le vrai diff git de tout ce qui a changé pour ce commentaire — cascades comprises (un commentaire peut toucher plusieurs pages) ;
- approuver → résolu, ou rejeter → rouvert avec votre raison, que l’agent lit à sa prochaine passe ;
- le diff s’affiche en vue unifiée ou côte à côte, et un badge Review dans l’en-tête compte ce qui attend.
L’utiliser depuis n’importe quel agent
Le protocole est une spec en texte brut, et notabene init l’installe dans votre repo
pour qu’aucun agent n’ait à la chercher :
<store>/protocol.md— la spec complète, committée à côté des commentaires qu’elle décrit. Hors ligne, sans npm, sans réseau. C’est ce fichier que vous montrez à un agent.AGENTS.md— un bloc borné<!-- notabene:begin -->…<!-- notabene:end -->qui indique aux agents qui le lisent au démarrage (Codex CLI, Cursor, Gemini CLI, Zed, Amp…) où vivent les commentaires et le protocole. Rien hors des marqueurs n’est touché ; désactivable viainit --no-agents-md.
Les deux se rafraîchissent en relançant notabene init (idempotent) — à faire après avoir
déplacé le store ou renommé un espace, et notabene doctor signale la dérive. Ce texte,
c’est la page protocole agent de ce site — la version
canonique, avec un jumeau Markdown dans les builds publics pour les agents qui naviguent ;
npx -y @z29k/notabene@latest protocol l’imprime hors ligne.
Dans Claude Code, la skill du plugin est ce protocole : elle se déclenche sur « traite les commentaires de la doc » et n’a besoin d’aucun AGENTS.md. La forme du store est elle-même un contrat public versionné — voir la référence du store.
Éditer dans la page
Éditer dans la page
La boucle de revue donne à un agent un moyen d’écrire. Ceci en donne un à l’humain, sans quitter la page qu’il est en train de lire.
Au survol d’un paragraphe, trois poignées apparaissent dans la marge — ✎ édite le bloc, + en ajoute un dessous, ⋮⋮ ouvre le menu du bloc. Un clic sur le crayon et vous éditez ce bloc, en place : il conserve la typographie de la page et ne bouge pas, il prend seulement un fond teinté pour qu’on voie lequel est actif. Seule la source de ce bloc est réécrite — le reste du fichier n’est pas touché, donc le diff que relisent les collègues est la ligne réellement modifiée.
C’est un outil de dev uniquement, exactement comme le commentaire : l’API d’écriture
n’existe que sous notabene dev. Un site construit ou publié n’a ni éditeur, ni endpoint,
ni la moindre trace de l’un ou de l’autre.
Les gestes
Deux intentions, deux gestes — d’où l’absence de bascule de mode :
| Action | Effet |
|---|---|
| ✎ dans la marge | vous éditez ce bloc |
| ⋮⋮ dans la marge | le menu du bloc — ajouter dessous, dupliquer, copier le lien, commenter, supprimer |
| Sélection de texte, partout | la bulle de commentaire, comme avant |
| Sélection pendant l’édition | la barre de mise en forme, à la sélection — Transformer en en tête |
| + dans la marge | un nouveau bloc sous celui-ci |
/ pendant l’édition | la palette de blocs — elle insère dessous ; taper pour filtrer |
Terminer, ou ⌘↵ | enregistrer — la modification est écrite |
| Clic ailleurs | un bloc intact se ferme ; un bloc modifié reste ouvert et pose la question |
| Annuler, ou Échap | abandonner — le bloc revient tel qu’il était |
⌘Z | annuler la frappe, comme partout |
⌘⇧M | passer ce bloc en Markdown brut, et revenir |
Lecture et édition ne se disputent jamais le même geste : sélectionner du texte veut
toujours dire « commenter ceci », et éditer part toujours du crayon. Écrire est un geste
explicite : seul Terminer (ou ⌘↵) touche au dépôt. Cliquer ailleurs ferme un bloc
intact — se déplacer dans la page n’écrit jamais — mais un bloc modifié reste ouvert et
pose la question : un clic égaré ne peut ni écrire votre modification, ni la perdre.
Abandonner est la seule sortie qui jette du travail : quand le bloc a des modifications non enregistrées, une confirmation est demandée — appuyer à nouveau, ou cliquer sur Annuler — là où le regard est déjà. Sur un bloc intact, la fermeture est immédiate. Rien n’est envoyé au serveur dans les deux cas, et le brouillon est supprimé : rouvrir le bloc redonne le texte du fichier.
Pendant l’édition, une carte compacte se place directement sous le bloc : Terminer, Annuler, l’annulation de frappe et la bascule Markdown sur sa première ligne, puis — dès que quelque chose a changé — tout ce que la sauvegarde peut emporter (voir Fermer la boucle). Il n’y a aucun autre chrome : pas de mode, pas de rail, pas de panneau ailleurs à l’écran.
Les tableaux portent leurs commandes sur la grille elle-même, pas sur une barre qui
suit le curseur. Survoler une cellule fait apparaître une poignée sur sa ligne et sur sa
colonne ; la poignée de colonne ouvre l’alignement (:--, :-:, --: — une propriété de
colonne en Markdown) et la suppression, celle de ligne la suppression, et les bords du
tableau offrent des boutons + pour une nouvelle ligne ou colonne. Lignes et colonnes
se réordonnent en les faisant glisser — le tout à l’intérieur du seul bloc que possède
l’éditeur.
Deux actions de la barre d’outils font lire toute une ligne ou toute une colonne comme un en-tête — elles apparaissent quand la sélection est dans un tableau. Elles existent sous contrainte : Markdown ne transporte aucun style, la seule chose qu’elles peuvent écrire est du gras. Elles mettent donc en gras chaque cellule de la ligne ou de la colonne, et le rendu donne à une ligne ou une colonne entièrement en gras la surface de l’en-tête. Un second appui annule. La ligne d’en-tête elle-même est épargnée : c’en est déjà une.
Ce seuil est voulu. Une cellule isolée en gras reste de l’emphase : | **✎** dans la marge | … | n’est pas un libellé, et le teinter serait une devinette. Seule une série
complète est traitée comme une décision — ce que produisent précisément les boutons. Le
fichier reste portable dans les deux cas : sur GitHub ou dans n’importe quel éditeur, ces
cellules se lisent simplement en gras.
Au-delà de ça, il n’y a pas d’option d’en-tête ni de pied, et c’est le format qui le veut, non un oubli :
un tableau GFM a exactement une ligne d’en-tête, toujours, et Markdown ne connaît ni
ligne de pied ni colonne d’en-tête. Les proposer supposerait d’émettre des tableaux HTML
bruts — qui cessent d’être du Markdown, cessent de passer la vérification de confinement, et
cessent de s’afficher partout ailleurs où vos fichiers .md sont lus.
La barre d’outils, elle, n’apparaît qu’à la sélection de texte — un simple curseur
n’obtient rien. Une barre qui suit le curseur se pose sur le texte même qu’on édite ; la
structure vit donc ailleurs : les tableaux sur leur grille, les listes au clavier (Tab /
⇧Tab pour indenter et désindenter) et dans la barre quand du texte y est sélectionné.
Tab passe de cellule en cellule dans un tableau — sur la dernière cellule il ajoute une
ligne au lieu de faire sortir du bloc.
Un tableau édité revient dans la convention du fichier, jusqu’à la ligne de séparation : un
fichier en | --- | compact le reste, un fichier aligné reste aligné. Ce n’est pas un
détail : un tableau incapable de faire l’aller-retour serait réécrit en entier par
quelqu’un qui s’est contenté de l’ouvrir et d’appuyer sur Terminer.
Les blocs
Un bloc vide le dit de lui-même : il porte un texte indicatif Taper « / » pour les commandes, comme celui de Notion. Un geste qu’il faut apprendre dans une documentation est un geste que la plupart des gens ne trouvent jamais.
/ ouvre la palette que Notion a appris à tout le monde, avec le verbe de Notion : elle
insère un nouveau bloc sous celui où l’on est — Texte, Titre 1 à 4, Liste à puces,
Liste numérotée, Liste de tâches, Citation, Code, Tableau, Séparateur, Image, chacun
avec son raccourci Markdown affiché à côté. Seul un bloc vide est typé en place, le seul
cas où insérer et transformer veulent dire la même chose. Taper pour filtrer, ↑/↓ pour
se déplacer, ↵ pour appliquer ; le /requête saisi est absorbé.
Changer ce qu’un bloc existant est vit dans la barre d’outils : sélectionner du texte, et la barre commence par Transformer en — le type courant du bloc, avec le menu de tout ce que GFM sait en faire. Deux verbes, deux surfaces, jamais confondus.
Gérer le bloc, c’est le menu ⋮⋮, et il ne demande aucune session d’édition : dupliquer et supprimer sont des écritures de plage en un coup (dupliquer écrit le bloc deux fois, supprimer n’écrit rien et emporte un séparateur — les voisins reviennent identiques octet pour octet, et supprimer demande confirmation avant d’agir) ; copier le lien du bloc met l’ancre du titre le plus proche dans le presse-papier ; commenter confie le bloc au flux de commentaire par sélection.
Ce que Notion propose et que Markdown ne sait pas transporter : la couleur, l’alignement de bloc (il n’y a pas de text-align en Markdown ; seules les colonnes de tableau ont un alignement, réglé depuis la poignée de colonne), monter/descendre (cela réécrit deux blocs à la fois, ce que la vérification de confinement refuse par construction), les encadrés, les blocs dépliants et les colonnes.
La barre de mise en forme couvre ce que GFM possède : gras, italique, code,
barré, liens — le bouton lien ouvre un petit champ pour l’URL, et survoler un lien
existant propose de l’éditer, le copier ou le retirer — et un bouton effacer la mise en
forme qui retire toutes les marques de la sélection. Les boutons s’allument quand la
sélection porte déjà leur marque. Le souligné, la couleur et le surlignage n’ont pas de
syntaxe Markdown : ils ne sont pas proposés plutôt qu’écrits en HTML en douce.
Les raccourcis Markdown fonctionnent aussi, et l’ont toujours fait : - , 1. , # ,
> , ```, , et |3x2| pour un tableau 3×2. La palette existe parce qu’un
raccourci qu’il faut déjà connaître n’est pas une interface.
+ dans la marge, à côté du ✎, démarre un nouveau bloc sous celui-ci. Le bloc à côté
duquel on clique reste rendu — ce n’est pas lui qu’on édite — et une surface vide s’ouvre
en dessous, prête pour /. La laisser vide n’écrit rien du tout : cliquer sur + puis changer
d’avis ne coûte rien.
Sous le capot, la sauvegarde réécrit la plage de ce seul bloc avec deux blocs — d’où des voisins qui reviennent identiques octet pour octet. (Éditer l’original pour taper en dessous était la première version, et cela se lisait comme un saut de ligne ajouté au bloc.)
Images : coller, ou choisir Image… dans la palette. Dans les deux cas le fichier est
écrit dans le dépôt à côté de la page, sous un nom au hash du contenu, et le lien est
inséré — il arrive donc dans le même commit que la prose qui le référence.
Ce qui n’a délibérément pas été repris de Notion : le glisser-déposer pour réordonner. Déplacer un bloc au-delà de son voisin réécrit deux blocs à la fois, ce que la vérification de confinement refuse — et c’est elle qui garde les diffs à la ligne réellement modifiée.
Les blocs de prose s’ouvrent en texte enrichi : ce que vous tapez ressemble à ce que la
page affichera, et sélectionner à l’intérieur fait apparaître la barre de mise en forme.
Les blocs de code (et tout ce que le rendu ne sait pas représenter comme de la prose)
s’ouvrent en Markdown brut — une vue WYSIWYG d’un bloc de code serait un moins bon
éditeur de code qu’un simple champ, et la page re-rend le vrai résultat dès
l’enregistrement. ⌘⇧M bascule dans les deux sens.
Si un rechargement vous interrompt — le HMR se déclenche à chaque sauvegarde, et dès que l’agent écrit — le texte déjà saisi est conservé et restauré à la réouverture du bloc.
Sur téléphone, les deux mêmes étapes survivent, avec le geste dont un téléphone dispose : un tap arme le bloc — il le souligne et fait apparaître deux boutons sous la barre du haut, Éditer ce bloc et ⋮ pour le menu du bloc (ajouter un bloc dessous, dupliquer, copier le lien, commenter, supprimer — en panneau bas). Un tap seul n’édite jamais rien, car sur téléphone le tap est le geste de lecture : on tape en défilant, en visant un lien, ou avant un appui long. L’appui long sélectionne toujours, et propose toujours de commenter.
Pendant l’édition, les outils d’écriture vivent dans une barre posée sur le clavier, là où sont déjà les pouces — la forme de Notion. La zone défilante agit sur le contenu : + (la palette de blocs, en panneau), Transformer en, gras, italique, barré, code, lien, désindenter et indenter, l’annulation de frappe et la bascule Markdown. Les deux sorties de session — Annuler et Terminer — sont regroupées à droite, derrière un léger filet. Pas de barre flottante au doigt — elle passerait sous la bulle de sélection native, et chaque marque est sur la barre en permanence. La carte de session ne garde que son corps et s’ancre juste au-dessus de la barre — et pendant la frappe elle reste à l’écart : avertissements et erreurs apparaissent d’eux-mêmes, mais la paperasse attend la sauvegarde. Enregistrer se fait en deux temps : sur un bloc modifié, appuyer sur Terminer fait apparaître la note de journal et les commentaires que cette sauvegarde ferme, juste au-dessus de la barre, et le bouton devient Confirmer — un second appui écrit, avec la note si elle a été remplie. Reprendre la frappe (ou ✕) replie la question. Au doigt, la sortie est toujours explicite : défilez et tapez librement — seuls le ✕ et Terminer de la barre closent la session.
Tout ce qui entoure le bloc reste rendu pendant la saisie : le rail de commentaires, les surlignages, le sommaire, les diagrammes. C’est tout l’intérêt — le geste visé est de lire un commentaire tout en corrigeant la phrase qu’il concerne.
Fermer la boucle
Dès que quelque chose a réellement changé, la carte sous le bloc s’étoffe : les commentaires ouverts de la page, et de quoi décrire la modification. Cocher ceux auxquels elle répond les passe en resolved dans la même sauvegarde, liés à une entrée de journal — le registre même où écrit une passe d’agent. Rien ne s’affiche tant qu’il n’y a rien à y rattacher.
En mode review: "approve", un commentaire fermé ainsi passe directement en resolved, pas
addressed : la page a été éditée, donc le validateur qu’attend ce mode, c’est vous. Sur la
page /review, chaque carte porte un lien Corriger dans la page qui ouvre directement le
bloc concerné, en mode édition — approuver en corrigeant, en un clic.
notabene comments verify audite ce qui a été écrit exactement comme il audite une passe
d’agent.
Ce qu’il refuse de faire
L’éditeur écrit dans le contenu : il est donc délibérément difficile de lui faire faire autre chose que ce qui était voulu.
- Il ne perturbera pas un bloc voisin. Après insertion, il re-parse le fichier et vérifie que tous les autres blocs top-level reviennent octet pour octet. Transformer un paragraphe en liste à côté d’une liste existante fusionnerait les deux ; une fence non terminée avalerait le reste de la page. Les deux sont refusés, rien n’est écrit. Quand la fusion est justement ce qu’on voulait, le refus propose d’inclure le bloc suivant et de réessayer.
- Il n’éditera pas ce qu’il ne sait pas représenter. Seuls les blocs que le rendu a pu
marquer sont éditables ; le HTML brut et le JSX restent en lecture seule et ne
s’éclairent jamais. Les fichiers
.mdxne sont pas éditables du tout : leurs offsets ne sont pas dans le même repère, l’éditeur décline plutôt que de deviner. - Il ne touchera pas un espace que vous avez fermé.
roots[].edit: falsepasse un espace en lecture seule, etedit: { enabled: false }retire l’éditeur partout. - Il n’écrira pas un fichier que git ne suit pas, car la modification serait alors
irrécupérable. Le message indique de faire
git add.edit: { requireGit: false }lève la garde si l’on tient à éditer hors gestion de version. - Il prévient avant de casser une ancre. Si la modification retire le texte que cite un commentaire, la carte sous le bloc le signale pendant la saisie — ce commentaire deviendrait orphelin.
Images
Coller une image dans l’éditeur l’écrit dans le dépôt, à côté de la page, sous un nom suffixé d’un hash de contenu, et insère le lien Markdown. Elle atterrit dans le même commit que la prose qui la référence. PNG, JPEG, GIF, WebP, AVIF et SVG, jusqu’à 8 Mo — le reste est refusé ; coller deux fois la même capture réutilise un seul fichier.
Ce qu’une sauvegarde ne peut pas vérifier à votre place
Une passe d’agent se termine par un build, notabene lint et vos commandes verify[].
Une édition humaine ne fait rien de tout cela — et la sauvegarde ne prétend pas le
contraire : elle répond enregistré ou elle refuse, rien entre les deux. Les
vérifications exhaustives restent où elles ont toujours été — notabene lint pour les
liens, vos verify[] en CI et dans chaque passe d’agent. L’éditeur n’exécute
délibérément pas vos commandes depuis le serveur de dev.
Configuration
export default {
edit: {
enabled: true, // défaut — false masque entièrement l'éditeur
requireGit: true, // défaut — refuse d'écrire un fichier non suivi par git
},
roots: [
{ key: "reference", path: "docs/reference", edit: false }, // espace en lecture seule
],
};
notabene doctor rapporte l’état, y compris la seule combinaison qui refuserait toute
sauvegarde : édition activée, requireGit activé, et pas de dépôt git.
Ce que ce n’est pas
Pas un CMS. Aucun éditeur dans un site déployé, pas de médiathèque, pas d’édition du frontmatter, pas de collaboration temps réel — git est la couche de fusion. Éditer une page dans une langue ne touche pas ses traductions : celles-ci restent un acte explicite.
Doc multilingue
Doc multilingue (i18n)
Ajoutez i18n pour servir les mêmes docs en plusieurs langues avec des URL propres
préfixées (la locale par défaut sans préfixe, les autres en /<locale>/…), un
sélecteur de langue dans l’en-tête, des alternates hreflang et un chrome par
page — une page française affiche nav, boutons et dates en français.
i18n: { locales: ["en", "fr"], defaultLocale: "en", strategy: "directory" },
Deux organisations des fichiers
Choisissez l’organisation des fichiers avec strategy :
directory(par défaut) — un dossier par locale :docs/en/guide.md·docs/fr/guide.md.suffix— une seule arborescence, traduite fichier par fichier :docs/guide.md(défaut) ·docs/guide.fr.md. Idéal pour ajouter des langues à une doc existante : les fichiers de la langue par défaut ne bougent pas, donc leurs URL et leurs fils de commentaires sont préservés.
Préférence de langue & repli
Le sélecteur enregistre la langue choisie par le visiteur ; dès lors, arriver sur une
page écrite dans une autre langue qui a une traduction redirige vers celle-ci —
suivre n’importe quel lien vous garde dans votre langue. Une page sans traduction
se replie sur la langue source et affiche un bandeau discret. Les pages non liées à une
langue de contenu — /comments, /journal, /review, la page d’accueil et la
404 — suivent votre langue courante côté client et embarquent le même sélecteur.
Tout est par langue
- Les commentaires sont par langue — un commentaire sur la page FR est un fil à part, relié au fichier source FR.
- La recherche et l’export PDF (
notabene pdf --locale fr) sont limités à une langue ; un site public embarque desllms.txtet des doubles Markdown par locale. - Toute chaîne humaine de la config accepte une map par locale : les
label/descriptiond’un espace (label: { en: "Docs", fr: "Documentation" }), la page d’accueil personnalisée (home: { en: …, fr: … }), et chaque libellé de lien de navigation, titre du bloc sidebar et ligne de pied de page. Non défini pour une locale → repli sur celle par défaut.
Omettez i18n pour une seule langue — le comportement est inchangé.
Export PDF
Export PDF
Transformez n’importe quelle page, n’importe quel dossier, n’importe quel espace ou la doc entière en un document soigné. Deux chemins, un même rendu optimisé pour l’impression (page de couverture + table des matières cliquable, palette forcée en clair pour que les diagrammes en mode sombre restent lisibles sur papier).
Dans le navigateur — zéro dépendance
Le menu Export PDF de l’en-tête propose la page courante, son dossier, son espace
ou la doc entière. Il ouvre une vue /print dans un nouvel onglet et déclenche
automatiquement l’Enregistrer au format PDF de votre navigateur. Les routes /print
sont statiques — elles existent en dev et dans tout build (y compris les
sites publics).
notabene pdf — l’artefact haute fidélité
notabene pdf --scope space:docs --out docs.pdf
Construit le site, pilote un Chromium headless et écrit un PDF avec un vrai plan de
signets (le panneau latéral navigable) et des numéros de page en pied. Flags :
--scope doc|space:K|folder:K/P|page:K/I, --locale, --out, --chrome.
Requiert la peer dependency optionnelle puppeteer (ou puppeteer-core plus
--chrome <path> / PUPPETEER_EXECUTABLE_PATH pointant vers un Chrome système) :
npm i -D puppeteer
Réglages
pdf: { enabled: true, pageSize: "A4", margin: "18mm" },
enabled: false masque le menu Export et retire les routes /print.
pageSize/margin alimentent la boîte CSS @page. Couvertures et titres de section
réutilisent les labels et l’ordre de la sidebar, si bien que le
PDF se lit dans le même ordre que le site.
Publier un site public
Publier un site public
L’app de revue est un outil local de dev — mais les docs qu’elle rend méritent
souvent un foyer public. build --public produit un artefact purement statique, en
lecture seule fait pour ça :
notabene build --public --site https://you.github.io --base /your-repo --out ./_site
Ce site même est construit ainsi — les docs de notabene, rendues et publiées par notabene.
Ce qui reste, ce qui part
- Tout l’interactif disparaît — structurellement. Pas d’UI de commentaires, pas
d’invite d’identité, pas de
/comments//review//journal, pas d’API, et rien du store.notabene. Les routes ne sont pas verrouillées ; elles ne sont pas construites. - Ce qui reste est l’expérience de lecture complète : nav, recherche, Mermaid +
lightbox d’images, mode sombre, i18n (pages par locale,
sélecteur,
hreflang), routes print/PDF,404. - Né lisible par les agents. Chaque page embarque un double Markdown à
<page>/index.md(annoncé via<link rel="alternate" type="text/markdown">), le site embarque/llms.txt(un index machine de chaque page, par locale) et/llms-full.txt(la doc entière en un seul document Markdown dans l’ordre de lecture), plusrobots.txt, un sitemap, des URL canoniques, des meta OpenGraph/Twitter et du JSON-LD. Essayez ici : /llms.txt.
Recherche plein texte (optionnelle)
Le site public hérite de la recherche intégrée telle quelle. Installez
Pagefind en dépendance de dev et build --public la promeut en
recherche plein texte statique :
npm i -D pagefind
Le build indexe l’artefact final : stemming par langue (un visiteur de /fr/ cherche
dans un index français, avec les formes fléchies du français), extraits surlignés sous
chaque résultat, et un poids réseau qui reste faible quand la doc grandit — le
navigateur ne télécharge que les morceaux d’index utiles à la requête. Zéro
configuration, même champ de recherche. Non installé → le site public garde la
recherche JSON intégrée. Et le contenu privé ne peut pas fuiter dans l’index :
l’indexation s’exécute sur l’artefact, où les
pages hors périmètre n’existent pas.
La même dépendance améliore aussi l’app de revue notabene dev : son index est
construit en direct depuis vos sources Markdown et rafraîchi quand elles changent —
aucun build en jeu. Deux différences propres au dev : le site de dev (donc son index)
inclut vos pages privées, et les résultats profonds par section (ancres de titres)
restent réservés au public.
Où aller ensuite
- Configurer
publish—site,base,exclude, avec exemples. - Garder du contenu privé — portée par espace / sous-arborescence / page.
- Domaine géré côté serveur — omettre
site, rester agnostique à l’origine. - Déployer via GitHub Pages — le workflow prêt à l’emploi.
La boucle de dev est intacte : notabene dev et le simple notabene build se
comportent exactement comme avant — la publication est opt-in, par build.
Configurer publish
Configurer publish
Tout vit sous une seule clé de config optionnelle — les flags CLI (--site/--base)
la surchargent, et --out copie l’artefact vers un chemin stable (il refuse d’écraser
quoi que ce soit qu’il n’a pas généré) :
// notabene.config.mjs
export default {
// …
publish: {
// ORIGINE déployée. Grave des URL absolues dans canonical, og:url, JSON-LD,
// hreflang, llms.txt, le sitemap et la ligne Sitemap de robots.txt.
// OPTIONNELLE — omettez-la pour garder le domaine hors du repo (voir
// « Domaine géré côté serveur »). Origine seule, sans chemin : un sous-chemin va dans `base`.
site: "https://you.github.io",
// Sous-chemin quand le site est servi sous un préfixe (site de projet
// GitHub Pages → "/<repo>"). Préfixe chaque lien et URL d'asset — contrairement
// au domaine, un sous-chemin affecte toujours le rendu, il ne peut pas être côté serveur.
base: "/your-repo",
// Sous-arborescences à garder hors des builds publics — globs comparés à
// `<space key>/<page id>` (indépendant de la locale : un motif masque chaque
// traduction d'une page). `*` = un segment de chemin, `**` = toute profondeur.
exclude: ["docs/internal/**", "docs/*/draft"],
},
};
Trois configurations typiques
publish: { site: "https://you.github.io", base: "/my-repo" } // GitHub Pages, site de projet
publish: { site: "https://docs.example.com" } // domaine personnalisé à la racine
publish: { exclude: ["docs/internal/**"] } // domaine gardé hors du repo
La troisième est le mode agnostique à l’origine — le même artefact derrière n’importe quel domaine.
Métadonnées par page
Deux clés de frontmatter alimentent les surfaces publiques (voir la référence complète du frontmatter) :
---
description: One-line summary — becomes the meta description / OpenGraph.
publish: false # cette page ne part jamais dans un build public
---
Exclure du contenu du build a sa propre page : garder du contenu privé.
Garder du contenu privé
Garder du contenu privé
Trois niveaux, du plus grossier au plus fin — chacun vit là où vit ce qu’il délimite.
Un espace entier — marquez l’entrée roots[] :
roots: [
{ key: "docs", label: "Docs", path: "docs" },
{ key: "notes", label: "Notes", path: "notes", publish: false }, // l'espace entier reste privé
],
Une sous-arborescence — des globs publish.exclude sur <space key>/<page id>
(c’est le chemin d’URL de la page sans préfixe de locale, donc un seul motif masque
toutes les traductions) :
publish: { exclude: ["docs/internal/**", "docs/*/draft"] },
Une seule page — son propre frontmatter :
---
publish: false # cette page n'apparaît jamais dans un build public
---
Un lien de navigation — pas du contenu, mais la même idée : une
entrée nav marquée publish: false reste en
dev et n’atteint jamais l’artefact (un dashboard, un wiki interne) :
nav: { header: [{ label: "Dashboard ops", href: "https://ops.internal", publish: false }] },
La garantie
Le contenu privé n’est pas caché, il n’est pas construit — pas de route (l’URL
renvoie un 404), pas d’entrée dans la sidebar, pas de résultat de recherche, pas de
ligne llms.txt, pas de double .md, pas d’entrée de sitemap, pas d’inclusion
print/PDF, et le nom et le chemin de l’espace n’atteignent jamais le HTML public.
notabene dev et les builds normaux montrent toujours tout — vous
passez en revue vos docs privées exactement comme le reste. La
notion de publish n’existe que pour les builds publics.
Une réserve : les liens vers du contenu privé
Si une page publique pointe vers une page privée, ce lien renvoie un 404 dans
l’artefact public — le build ne le réécrit pas et n’émet aucun avertissement.
notabene lint attrape exactement cela : lancez-le après build --public et chaque
lien d’une page publique vers du contenu exclu par le scoping est signalé (la vérité des
routes publique ne contient tout simplement pas ces pages — voir la
référence CLI).
Domaine géré côté serveur
Domaine géré côté serveur ? Omettez site
Quand le domaine public est l’affaire du serveur — un vhost ou un reverse proxy en
frontal, plusieurs miroirs, ou un domaine pas encore choisi — ne définissez simplement
pas site (et ne passez pas de --site). L’artefact devient agnostique de
l’origine : pas une seule URL absolue n’y est embarquée, la même sortie fonctionne
donc derrière n’importe quel domaine, et changer de domaine n’exige jamais de rebuild.
Ce qui change
| Surface | Avec site | Sans |
|---|---|---|
llms.txt / llms-full.txt / doubles .md | URL absolues | chemins relatifs à la racine (les agents les résolvent contre l’origine depuis laquelle ils les ont récupérés) |
alternates hreflang | absolues | par chemins |
canonical, og:url, JSON-LD | émis | non émis — ils n’ont de sens qu’avec une origine |
sitemap + ligne Sitemap: de robots.txt | émis | non émis — les spécifications exigent des URL absolues |
Ce que cela coûte, concrètement
La visibilité dans les moteurs de recherche — rien d’autre. Sans sitemap, URL
canoniques ni hreflang valide, les crawlers ne découvrent les pages qu’en suivant les
liens, rien ne consolide les doublons si la doc répond sur plusieurs domaines, et les
pages multilingues n’envoient aucun signal de langue aux moteurs de recherche. Les
aperçus de liens sociaux gardent leur titre/description mais perdent la carte d’URL.
Les lecteurs humains et les agents IA ne perdent rien — chaque page, double et
fichier llms fonctionne à l’identique.
Règle simple : hébergement interne, miroir, ou domaine pas encore arrêté → omettez
site ; un site public dont le référencement compte → définissez site.
base reste indépendant
Définissez base dès que le site vit sous un sous-chemin, avec ou
sans domaine — un sous-chemin affecte toujours les liens rendus, il ne peut donc pas
être laissé au serveur.
Déployer via GitHub Pages
Déployer via GitHub Pages
Déployez sur n’importe quel hébergement statique. Pour GitHub Pages : Settings → Pages → Source = GitHub Actions, puis :
# .github/workflows/docs.yml
name: docs
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npx notabene build --public
--site https://${{ github.repository_owner }}.github.io
--base /${{ github.event.repository.name }}
--out ./_site
- uses: actions/upload-pages-artifact@v3
with: { path: ./_site }
- id: deployment
uses: actions/deploy-pages@v4
Notes :
- Avec
publish: { site, base }défini dans votre config, les flags--site/--basepeuvent être omis entièrement. - Hébergement sur un domaine personnalisé ou un site racine utilisateur/organisation ?
Retirez
--baseet pointez--sitevers votre domaine. - L’artefact embarque un
.nojekyll, donc les assets_astro/survivent même à l’hébergement classique par branche gh-pages. npm i -D pagefinddans votre repo et ce même workflow embarque aussi la recherche plein texte —npm cil’installe, le build la détecte. Rien à changer ici.- Ce site de documentation est déployé par exactement ce workflow — voyez-le dans le repo.
Référence
Référence
Référence
Le pendant exhaustif du Guide — tableaux et contrats, une page par surface :
- CLI — chaque commande et flag
notabene. - Clés de configuration — toute la surface de
notabene.config.mjs. - Frontmatter — chaque clé qu’une page peut porter.
- Le contrat du store
.notabene— le schéma JSON versionné que les agents lisent et écrivent. - Le protocole agent — la boucle file-I/O-first que suit n’importe quel agent pour transformer les commentaires en éditions.
- Modèle de sécurité — pourquoi l’API d’écriture ne peut pas vous nuire.
CLI
CLI
Le package npm est scopé (@z29k/notabene) ; la commande installée est simplement
notabene, donc npx notabene … fonctionne tel quel dès lors que le package est
une dépendance de votre repo. Sans installation locale, utilisez toujours le nom scopé —
npx -y @z29k/notabene@latest … — car notabene non scopé n’est pas notre package.
| Commande | Ce qu’elle fait |
|---|---|
notabene doctor | État en lecture seule au format JSON : config/store/port/éditeur + dossiers de doc détectés — --json |
notabene init | Écrit notabene.config.mjs + crée le store (sans effet s’il existe déjà) ; --detect auto-détecte les dossiers de doc. Écrit aussi le point d’entrée agent : <store>/protocol.md + un bloc borné dans AGENTS.md — désactivables via --no-protocol / --no-agents-md. Idempotent : relancez-le pour rafraîchir les deux |
notabene dev | Démarre le serveur de revue sur les docs de ce repo (rechargement à chaud) ; --detach le lance en démon d’arrière-plan. Avec la dépendance de dev optionnelle pagefind, sa recherche devient plein texte aussi |
notabene status | Le serveur détaché tourne-t-il ? (pid, port, URL) — --json |
notabene stop | Arrête le serveur détaché |
notabene build | Construit le site (Node standalone ; docs prérendues, pas d’API d’écriture dans l’artefact) |
notabene build --public | Site statique en lecture seule pour hébergement public — voir le guide. [--site URL] [--base /sub] [--out DIR]. Avec la dépendance de dev optionnelle pagefind, l’artefact gagne une recherche plein texte statique |
notabene preview | Sert le site construit |
notabene lint | Valide les liens inter-docs contre les routes émises par le dernier build (suggestions « did you mean » ; --json). Après build --public, attrape aussi les liens de pages publiques vers du contenu privé. Exit 1 = liens cassés, 2 = pas encore de build |
notabene pdf | Exporte un PDF via Chromium headless (sommaire de signets + numéros de page) ; --scope doc|space:K|folder:K/P|page:K/I, --locale, --out, --chrome. Nécessite la peer dep optionnelle puppeteer (ou puppeteer-core + --chrome) |
notabene migrate | Convertit le store vers la disposition un-fichier-par-commentaire (estampille schemaVersion 3) |
notabene comments ls | Liste les commentaires — --open --json --page <p> (pour agents/scripts) |
notabene comments done | Marque des commentaires traités : done <id…> [--note <texte>] [--journal <entryId>]. Le statut vient de review (auto → resolved, approve → addressed) — --status force, --force agit sur un commentaire en attente (hold). Écriture atomique ; tous les autres champs préservés |
notabene comments reopen | Renvoie des commentaires à open : reopen <id…> [--reply <texte>] [--author <nom>] — la raison devient une réponse du thread que l’agent lira à la passe suivante (le pendant CLI du rejet sur /review) |
notabene comments verify | Audite le store : statuts, liens commentaire↔journal dans les deux sens, disposition des fichiers, doublons, pages disparues. --json ; exit 1 si erreurs, 2 sans store. À lancer après une passe d’agent, ou en CI |
notabene journal add | Ajoute une entrée de journal JSON lue depuis stdin (écriture atomique ; --json renvoie { id } pour chaîner côté agent) |
notabene protocol | Imprime le protocole agent sur stdout — --path affiche son chemin, --write rafraîchit <store>/protocol.md |
Flags globaux
| Flag | Signification |
|---|---|
--root <path> | Racine du repo consommateur (défaut : cwd) |
--config <path> | Chemin de la config (défaut : <root>/notabene.config.mjs) |
--port <n> | Port du serveur de dev (sinon la config port, sinon un port libre) |
--detach | dev uniquement : démon d’arrière-plan (géré par status/stop) |
--detect | init uniquement : préremplit roots[] avec les dossiers de doc trouvés |
--no-protocol / --no-agents-md | init uniquement : ne pas écrire la copie <store>/protocol.md / le bloc AGENTS.md |
--host | Expose sur le LAN — réseaux de confiance uniquement (sécurité) |
--public / --site / --base / --out | build uniquement : l’artefact de site public |
Clés de configuration
Clés de configuration
notabene.config.mjs est un module ES de données uniquement à la racine de votre
repo ; chaque clé est optionnelle. La version narrative, avec exemples, se trouve dans
le guide de configuration.
| Clé | Défaut | Signification |
|---|---|---|
siteName / tagline | "Docs" / "docs" | Marque de l’en-tête |
locale | "en" | Langue de l’UI + collation du tri de la nav |
format | "mdx" | "mdx" (.mdx strict + .md tolérant) ou "commonmark" (pas de MDX du tout). init génère "commonmark" |
roots[] | [{docs}] | Espaces de doc : { key, label, path, exclude, description, publish }. label/description acceptent une map par locale avec l’i18n ; publish: false garde l’espace hors des builds publics |
store | "docs/.notabene" | Dossier commentaires + journal — committez-le (contrat) |
home | — | Page d’accueil personnalisée : un fichier Markdown relatif au repo (ou une map par locale) rendu au-dessus des cartes d’espaces sur / |
branding | — | Assets d’identité : { logo, logoDark, favicon, socialImage }, fichiers relatifs au repo servis sous /_nb/…. Favicon non défini → une marque par défaut intégrée |
theme | — | Personnalisation du rendu : { tokens, css, assets, code } — surcharges de tokens --nb-* (validées ; une coquille lève une erreur), une feuille de style chargée après celle du renderer (sûre vis-à-vis des cascade layers), un dossier du repo servi sous /_nb/assets/… pour les polices/images (allow-list d’extensions, pas de traversée), un thème Shiki pour le code ("github-light" ou { light, dark } — les deux palettes suivent le sélecteur de schéma), et mermaid: false pour garder la palette de diagrammes propre à Mermaid |
nav | — | Liens sortants : { header[], sidebar: { title, links[] }, footer: { links[], text, poweredBy } }. Une seule forme d’entrée — { label, href, icon, iconOnly, publish } ; label accepte une map par locale, publish: false garde un lien hors des builds publics. Validé au chargement (allow-list de schémas, noms d’icônes, doublons) |
port | 3009 | Port d’astro dev |
host | false | true/NOTABENE_HOST=1/--host expose l’API d’écriture au LAN (sécurité) |
verify[] | [] | Vérifications post-édition que l’agent exécute (le build du renderer s’exécute toujours) |
review | "auto" | "auto" = l’agent résout les commentaires ; "approve" = l’agent propose (addressed), vous validez chacun sur /review avec un diff (boucle de revue) |
author | git user.name | Auteur de commentaire par défaut ; chaque navigateur le remplace par appareil via le dialogue d’identité |
authorEmail | git user.email | E-mail d’auteur par défaut ; intégré façon git (Name <email>) pour garder les identités uniques |
edit | { enabled: true, requireGit: true } | Éditeur dans la page (dev uniquement) : enabled affiche l’affordance et injecte l’API d’écriture ; requireGit: false autorise l’écriture d’un fichier non suivi par git. Par espace : roots[].edit: false le passe en lecture seule |
editPattern | — | Lien « Modifier cette page » sous chaque page : une URL avec un placeholder {path} (chemin source relatif au repo), ex. https://github.com/o/r/edit/main/{path}. Placeholder obligatoire — validé au chargement |
pdf | { enabled: true, pageSize: "A4", margin: "18mm" } | Export PDF — enabled active le menu Export + les routes /print ; pageSize/margin définissent la boîte @page |
i18n | — | Doc multilingue : { locales, defaultLocale, strategy: "directory"|"suffix" }. Omettez pour une seule langue |
publish | — | Cible du build public : { site, base, exclude }. site optionnel — omis = artefact agnostique de l’origine |
Surcharges CLI/env
--site/--base surchargent publish.site/publish.base ; NOTABENE_HOST=1 équivaut
à host: true ; la CLI passe l’identité git du repo comme repli pour
author/authorEmail. Rien d’autre n’est configurable hors de ce fichier.
Frontmatter
Frontmatter
Du YAML optionnel tout en haut d’une page. Tout a un défaut raisonnable — un repo sans aucun frontmatter se rend très bien (noms de fichiers humanisés, ordre alphabétique).
---
title: Internal network map # <title> de la page + fil d'Ariane (prime sur le premier H1)
description: Segments and VLANs. # builds publics : meta description + OpenGraph + JSON-LD
publish: false # builds publics : exclut cette page entièrement
lastUpdated: 2026-05-04 # pied de page : prime sur la date git « Mis à jour le »
sidebar:
label: Network map # texte de la sidebar (sinon title, sinon nom de fichier humanisé)
order: 9 # position parmi les pages sœurs (croissant)
indexLabel: Start here # pages d'accueil de dossier : renomme l'entrée « Aperçu »
---
| Clé | Effet |
|---|---|
title | <title> de la page, fil d’Ariane, titre du résultat de recherche. Se replie sur le premier # H1, puis sur le nom du fichier |
description | Builds publics : <meta name="description">, description OpenGraph/Twitter, JSON-LD |
publish: false | Builds publics : la page n’est pas construite — ni route, ni nav, ni recherche, ni llms, ni double, ni sitemap. Les builds dev/normaux la montrent toujours |
lastUpdated | Prime sur la date dérivée de git dans le Mis à jour le du pied de page. Toute date que YAML sait parser. Utile quand l’historique git induit en erreur (contenu importé ou généré) |
sidebar.label | Texte de l’entrée dans la sidebar. Résolution : sidebar.label → title → nom de fichier humanisé |
sidebar.order | Clé de tri parmi les pages sœurs, croissante. Les entrées sans valeur gardent l’ordre alphabétique, après celles qui sont ordonnées. Groupes et pages partagent un seul ordre |
sidebar.indexLabel | Sur la page d’accueil d’un dossier : renomme son entrée Aperçu localisée |
Dossiers
Un dossier est nommé et positionné par sa page d’accueil — <folder>/index.md (ou
readme.md) : son frontmatter sidebar s’applique au groupe entier, et la page
elle-même apparaît comme l’entrée Aperçu du groupe. Labels et ordre se propagent aux
fils d’Ariane et aux couvertures PDF.
Les clés inconnues sont ignorées et préservées — les agents qui éditent une page doivent garder le frontmatter existant intact (le skill de revue le fait).
Le contrat du store .notabene
Le contrat du store .notabene
Le store est un contrat public versionné — committé dans votre repo, lu et écrit par
les agents. Sa forme ne change jamais silencieusement : <store>/meta.json porte
{ "schemaVersion": n } (actuellement 3), et tout changement de forme s’accompagne
d’un migrateur (notabene migrate).
Disposition
<store>/
meta.json # { "schemaVersion": 3 }
journal.json # un tableau d'entrées de journal
protocol.md # le protocole agent (écrit par `init` ; ce n'est pas de la donnée)
<page>/<comment-id>.json # UN FICHIER PAR COMMENTAIRE → merges git sans conflit
meta.json, journal.json et protocol.md sont des noms réservés à la racine du
store ; tout le reste est de la donnée de commentaire. Les lecteurs ne parsent que du
.json, donc protocol.md est inerte — il voyage avec le store pour que la spec soit
toujours à côté des commentaires.
<page> est le chemin logique de la page (docs/guide/setup — avec l’i18n c’est l’id
brut encodant la locale, les commentaires sont donc par langue). Les anciens stores v1
(un tableau par page) sont toujours lus ; toute écriture migre la page concernée.
Un commentaire
{ "id", "space", "page", "scope", // scope : "selection" | "page" | "block"
"anchor": { // selection : citation de texte façon W3C
"quote", "prefix", "suffix", "section" // texte rendu + contexte + titre le plus proche
} | { "kind", "key", "label", // block (diagramme/image) : clé dérivée du contenu
"section", "index" } | null,
"thread": [{ "author", "body", "ts" }], // author peut être façon git "Name <email>"
"status": "open" | "addressed" | "resolved",
"hold": false, // true → l'agent l'ignore (WIP du relecteur)
"resolution": { "note", "journalEntryId" } | null,
"createdAt", "updatedAt" }
addressed est l’état de la revue en deux phases : proposé
par l’agent, en attente de validation humaine sur /review.
Une entrée de journal
{ "id", "date", // YYYY-MM-DD
"title", "summary",
"changes": [{ "page", "commentIds": [], "what", "why" }] }
Un enregistrement changes[] par page réellement touchée — le diff de /review est
construit en inversant le journal, donc une page qui n’y figure pas n’apparaîtra pas
dans le diff du relecteur. Le resolution.journalEntryId de chaque commentaire résolu
pointe vers son entrée.
Règles que les agents doivent honorer
- Les écritures sont atomiques (fichier temporaire + rename) — ne jamais écrire du
JSON partiel à la main. La CLI (
comments done/reopen,journal add) s’en charge ;comments verifyaudite le résultat. - Ne jamais supprimer le store en masse ; supprimez un commentaire précis par id si on vous le demande.
- Ne traiter que
status: "open"ethold: false. - Le protocole complet vit dans
<store>/protocol.md— écrit parnotabene init, committé avec le store, rafraîchi en relançantinit. Pointez-y n’importe quel agent.
Modèle de sécurité
Modèle de sécurité
Les API d’écriture touchent votre git — elles sont donc cloisonnées, par construction. Il y en a deux : l’API de commentaires, qui écrit le store, et l’éditeur dans la page, qui écrit votre source Markdown. Les règles ci-dessous valent pour les deux ; l’éditeur en ajoute une qui lui est propre.
- Dev uniquement. Le chemin d’écriture n’existe que sous
notabene dev. Enbuild/previewles mutations renvoient403, et un build public ne contient pas ces routes du tout. - Loopback par défaut. Le serveur se lie à
127.0.0.1; l’API d’écriture n’est pas joignable depuis votre réseau sans opt-in explicite via--host/NOTABENE_HOST=1— réseaux de confiance uniquement. - Chaque écriture est gardée au-delà du bind : les requêtes cross-origin sont
refusées (anti-CSRF), un en-tête
Hostnon-loopback est refusé en mode loopback (anti-DNS-rebinding), et — quand vous définissezNOTABENE_TOKEN— chaque écriture doit porter unx-notabene-tokencorrespondant. Définir un token est recommandé avec--host. - L’éditeur refuse d’écrire un fichier non suivi. Il touche votre contenu, pas
seulement le store : git est donc le seul retour arrière qu’il puisse offrir — et
notabene devn’exige pas un dépôt. Un fichier que git ne suit pas est refusé, bruyamment, avec le remède. Contournable viaedit: { requireGit: false }, ou éditeur désactivé entièrement viaedit: { enabled: false }. - Une identité par personne. Sur un hôte non-loopback, chaque visiteur est invité à renseigner son nom (+ e-mail optionnel) avant de naviguer, pour que les commentaires soient attribués à de vraies personnes plutôt qu’au défaut git du propriétaire du repo.
- L’agent ne committe jamais sans demander et ne supprime jamais le store en masse — cela fait partie du protocole.
- La CLI est une surface distincte. Les règles ci-dessus encadrent l’API d’écriture
HTTP. Les commandes qui écrivent dans le store (
comments done/reopen,journal add) sont des commandes locales que vous — ou un agent dans votre terminal — lancez délibérément : ni serveur, ni port, ni réseau. Écriture atomique, un commentaire à la fois, etcomments verifyaudite le résultat.
L’artefact public est l’image miroir : pas d’API d’écriture, pas de données du store, pas d’identité — rien à garder, parce que rien n’est construit.