15 — Export PDF : « les pages en PDF », pour tous les formats qui s'impriment
Statut : accepté · Phase : P6 à P8 · Mis à jour : 24/10/2026.
Accepté le 24/10/2026 (questions 1 à 6), questions 7 à 9 ouvertes. Phases PDF0 à PDF5 (§6) : PDF5 couvre les autres formats, chacun dans sa propre phase.
Ce document traite une demande transverse : pouvoir exporter les pages en PDF, et en particulier le
livre (book, book.recit.dev) et le webtoon (toon, webtoon.recit.dev), qui doivent l'avoir.
Il couvre aussi report, deck (qui a déjà un export, à unifier) et, en option, les pages et
sections de doc (docs et les autres sites). Les formats en cours de planification (glossary et
directory en 12-glossaire-annuaire.md, atlas et ::map en 13-cartes.md, form en
10-formulaires.md) prennent le même chemin dans leur propre phase.
Il dépend de 02-architecture.md (modèle Story), de 03-packaging.md (groupes de packages), de
06-roadmap.md (P6 à P8), de 07-domaines.md §4 et §5 (composants de site, pas de rendu lourd sur
Vercel), de 08-formats.md (§0, §1, §2, §3, §9) et de 09-langues-et-json.md (JSON typé, langues).
Chaque choix porte une recommandation. Les questions 1 à 6 du §8 ont été tranchées le
24/10/2026, sans l'utilisateur et à sa demande (décisions réversibles) ; les questions 7 à 9 restent
ouvertes.
Deux règles du projet, rappelées par l'utilisateur, encadrent tout le document :
- Tout Markdown a un jumeau JSON. Une option d'export écrite dans le frontmatter existe à l'identique dans le JSON de la story, validée par le même schéma.
- Un seul système de types, cohérent entre packages. Chaque option est un schéma zod de
@recit/core, son type TS en est dérivé (z.infer,z.output), et aucun package ne redéfinit un type partagé.
1. L'idée et ce qu'elle apporte
Une story Récit s'imprime comme elle se lit : la même source produit un PDF fidèle, paginé selon
son format, avec signets, liens, texte sélectionnable et métadonnées tirées de la story, par une
seule chaîne et une seule commande, recit export --to pdf.
- « Écrire une fois, raconter partout » jusqu'au papier. Le PDF est le format d'échange qui survit à tout : envoi par mail, archivage, impression, lecture hors ligne. Un livre sans PDF n'est pas un livre, et un webtoon sans PDF ne se partage qu'en lien.
- Une chaîne, pas neuf. Aujourd'hui, seul le deck sait sortir un PDF, avec un code et des types qui lui sont propres. Sans plan commun, chaque format réinventerait son lanceur de Chromium, son signal « prêt », sa feuille d'impression et ses options. Ce plan fixe une seule chaîne : le format ne fournit que sa mise en page imprimable, tout le reste est partagé.
- Les options sont des données. Taille de page, marges, table des matières, en-têtes : tout est typé une fois, s'écrit dans le frontmatter ou dans le JSON, se valide dans l'éditeur via le JSON Schema publié, et se surcharge en ligne de commande avec le même schéma.
- Accessible par défaut. PDF balisé et signets activés par défaut,
langetdirposés depuis la locale : un PDF Récit doit pouvoir être lu par un lecteur d'écran. - Les sites en profitent. Les fichiers générés en CI (jamais sur Vercel) sont listés dans un
manifeste d'exports typé, que lit l'
ExportPaneldes sites de format (07-domaines.md§4).
Liste fermée des formats qui exportent en PDF
On ne promet que ce qui a une mise en page imprimable définie. Les autres formats répondent par une
erreur explicite (format "motion" has no PDF export; use "recit render"), jamais par une capture
devinée.
| Format | PDF ? | Forme du PDF | Phase |
|---|---|---|---|
deck | oui | une page par slide (et une page de notes en option), taille du design | P6 (PDF0, PDF1) |
report | oui | A4 ou Letter, page de titre, table des matières, tableaux paginés | P6 (PDF1) |
toon | oui | pages de hauteur fixe, coupées entre les cases (§3.5) | P7 (PDF2) |
book | oui | livre paginé : chapitres, table des matières, notes, renvois, index (§3.6) | P8 (PDF3) |
docs | option | une page, ou une section de la sidebar (§3.7) | après P8, optionnel (PDF4) |
roadmap, calendar | oui | frise, mois ou agenda (spec dans leur phase) | P9–P10 |
glossary, directory | oui | liste A–Z, annexe (12-glossaire-annuaire.md, DI5.5) | phase de 12-glossaire-annuaire.md |
atlas et bloc ::map | oui | SVG net à l'impression (13-cartes.md) | bloc : lot P6 (GE1 à GE8) ; atlas : P15 |
form | oui | version imprimable avec QR code et URL (10-formulaires.md, FO6.1) | plus tard (FO6.1), après P13 |
motion | non | vidéo : recit render | — |
stories | non | images et vidéo | — |
2. Existant et constats
2.1 Ce que le code fait aujourd'hui
-
packages/cli/src/commands/export-pdf.ts, propre au deck :buildProject()dans un dossier temporaire ;- serveur statique
serveStatic()(http natif, port 0) ; - Chromium via Playwright, ouverture de
?print=1[¬es=1]; - attente de
window.recitReady === true(60 s par défaut) ; page.pdf({ width, height, printBackground: true, preferCSSPageSize: false })aux dimensions du design en pixels.
Le thème est imposé par
addInitScript(localStoragerecit-theme). Playwright est une dépendance pair optionnelle de@recit/cli(>=1.50,1.63.0en dev).loadPlaywright()essaieplaywright, puisplaywright-core, puis relance avecchannel: "chrome", c'est-à-dire le Chrome installé sur la machine, de version quelconque. -
Les types
PlaywrightPage,PlaywrightBrowseretPlaywrightModulesont écrits à la main dans la CLI.PlaywrightPage.pdfn'y déclare nitaggednioutline. -
PdfExportOptions(output,configPath,locale,notes,theme: "light" | "dark",timeout) est un type TS manuel, sans schéma zod. Son champthemeredéfinitThemeMode(ThemeModeSchemadanspackages/core/src/config/validate.ts).PdfExportResult.pageCountvautbuild.totalSlides. -
packages/cli/src/commands/export-html.ts: HTML autonome du deck, avertissement au-delà de 10 Mo. -
packages/cli/src/index.ts(l. 162-214) : une seule commandeexport [file](défautslides.md, description « Export the deck »), PDF par défaut,--html,--notes,--theme,--locale,-c. Pas de sous-commande, pas d'aiguillage surformat:. -
Mode impression du deck :
packages/formats/deck/src/deck/navigation-utils.ts:DeckModeinclut"print", déduit de?print=1ou?mode=print;packages/formats/deck/src/deck/Presentation.tsx(l. 265-305) : une.print-slidepar slide,revealAll,.print-notesen option,onPrintReadyquand toutes les slides sont prêtes etdocument.fonts.readyrésolu ;packages/formats/deck/src/runtime/DeckApp.tsxposewindow.recitReady = true. Ce contrat n'existe que dans le deck.
-
packages/theme/src/print.css:@media print { @page { margin: 0 } }global, sauts sur.slide-container,.print-slideet.print-notes, animations coupées, UI du deck masquée,pre-wrapsur le code. Seulpackages/cli/src/bundle/css.tsl'importe (et le service worker du deck). Le fichier est donc propre au deck, mais@recit/themel'exporte comme s'il était générique. Ses commentaires sont en français (règle : code en anglais). -
packages/formats/{paged,book,toon,report,roadmap,calendar,stories}etpackages/{data,time}sont des stubs (export {}, privés,0.0.0). La description de@recit/pagedne cite que report et book. -
packages/core/src/schema/source.ts: seulsDeckSourceetDocsSourceont des réglages typés.DeckSettingsSourceSchemaporte les clés du frontmatter à plat (horsmeta).report,toon,booket les autres passent pargenericSource()(looseObject). -
packages/core/src/config/validate.ts(zod/mini) :RecitConfigSchemaest découpé par format (deck:,motion:), sans sectionpdf,printouexport. La section du site, qui publie les récits, estpublish:(PublishConfigSchema) ;stories:(StoriesConfigSchema) est la section du format stories, encore vide : les anciennes clés de site y sont refusées (§3.2). Il définit son propreLocaleSchema, doublon deLocaleTagSchema(schema/source.ts), etsource.tsimporte déjàconfig/validate.ts.STORY_FORMATS(types/index.ts) est dérivé deStoryFormatSchemadepuis le lot 8 de l'audit ; avant, c'était un tableau typé à la main, sans schéma zod. -
Jumeau JSON : l'aller-retour Markdown ⇄ JSON est dans
schema/convert.ts(markdownToSource,sourceToMarkdown), testé parpackages/cli/src/__tests__/stories.spec.ts.schema/parity.tsvérifie autre chose : la parité entre une variante de langue et la référence (checkStoryParity). -
Dépendances : ni
pagedjs, nifflate, nipdf-lib, nipdfjs-distdans le lockfile.pngjs5.0.0 y est (transitif). -
Le framework de sites expose des emplacements (
Slot.Target, dontcontent-before,content-after,navigation-after) et une API de plugin avecgetRoutes. En revanche, les pages MDX ne sont importées que dans le module virtuel généré par le cœur (packages/site/src/vite/plugin-docs.tsconstruitfileImportspourmarkdownPlugin). Une route d'impression qui rend d'autres pages MDX n'est donc pas acquise sans patch : c'est à démontrer (§3.7).
2.2 Ce que disent les plans
08-formats.md§0 annonce « tous → PDF via@recit/paged(P6–P8) », mais la colonne « Utilisé par » de@recit/pagedne cite que report, book et deck.- report (§1, R2 et R4) : extraire la logique PDF de la CLI vers
@recit/paged, ajouter Paged.js,@page, table des matières et en-têtes, migrer le deck. Le PDF du report y est décrit avec une « table des matières numérotée, en-tête courant avec le titre, pied avec la page ». Critère de P6 : « l'export PDF du deck passe par@recit/pagedsans régression ». - book (§3, B2 et B3) : réglages sous une clé
book:du frontmatter (trim: A5, A4, 5x8in, 6x9in ;chapters;front;back), marges miroir, titres courants, numérotation romaine puis arabe, chapitres sur page impaire, table des matières et index numérotés, EPUB 3 maison (fflate,epubchecken CI) en B3. Le report (§1) a de même une cléreport:(paper,toc). Le deck, lui, a ses réglages à plat. - toon (§2, P7) : exports « lecteur web, tranches PNG/WebP, motion ». Tranches en W3 : capture
Playwright et découpe
sharpdans@recit/toon. Aucun PDF prévu. 09-langues-et-json.md:settingsd'une story est « la section du format derecit.config.ts» (§2) ;--localeet--all-localessurbuild,exportetrender(§5) ; chaque format livre son schéma avec le format (L6) ; un PDF et un EPUB par langue pour book seulement. Rien sur le sens de reliure RTL ni sur l'écriture verticale CJK.07-domaines.md§4 rangeExportPanelparmi les composants de@recit/site-kit.10,12,13et14le déclarent chacun avec leurs propres types de sortie (JSON, CSV, PNG, MP4, ICS, Fountain…).13-cartes.mdexige l'attribution des fonds de carte jusque dans le PDF.- « Lancer Chromium et capturer » est revendiqué par plusieurs plans :
@recit/paged(08 §0),@recit/toon(08 §2, W3),@recit/cli(11-images.md, l. 55 et captureslocator.screenshot()), et13-cartes.mdreste ambigu. 07-domaines.md§5 : Chrome, Paged.js, Remotion et sharp tournent en CI, Vercel ne sert que du statique. Le texte ne dit pas comment les fichiers générés en CI arrivent sur Vercel.
2.3 Constats
- Le webtoon n'a pas de PDF. La demande l'exige : il faut l'ajouter au périmètre de P7.
- « Tous → PDF » n'est pas tenu. Il faut la liste fermée du §1.
- Aucun type commun.
PdfExportOptionsvit dans la CLI, redéfinitThemeMode, et n'a ni schéma zod ni jumeau JSON. C'est contraire aux deux règles rappelées en tête. - Le signal « prêt » et le mode impression n'existent que dans le deck. Chaque format les réinventerait.
print.cssmélange une base commune et des règles du deck. Son@page { margin: 0 }global est incompatible avec les marges d'un livre.- Deux mécanismes de taille de page. Le deck impose des pixels (
preferCSSPageSize: false), la pagination CSS repose sur@page { size }. Il faut un seul mécanisme, et des goldens du PDF du deck avant la migration (aucun n'existe). - Deux conventions de réglages de format. Le deck a ses réglages à plat (
settings), 08 met ceux du report et du livre sousreport:etbook:. Il en faut une seule (§3.3). - Plusieurs propriétaires pour « lancer Chromium ». Le PDF, les tranches du toon, les images
(
11-images.md) et les cartes (13-cartes.md) en ont besoin : il faut un seul module (§4). - Un
ExportPanel, mais un manifeste par type de sortie. Le composant est déjà unique (site-kit) ; son entrée doit l'être aussi (§3.2). - Le moteur du report n'est pas tranché entre 08 et ce plan. 08 veut Paged.js en P6 pour une table des matières numérotée ; Paged.js n'a plus de version stable depuis 2023 (§7). Décision proposée en §3.4.
3. Architecture proposée
3.1 Vue d'ensemble : une seule chaîne
Code
- La CLI charge la source (Markdown ou JSON, c'est la même
Story), la config et les flags. resolvePdfOptions()de@recit/corefusionne les couches brutes (entrées non parsées) puis valide le résultat une seule fois avec le schéma du format. Les défauts du schéma s'appliquent donc une seule fois, et une option invalide échoue avant de lancer Chromium, avec le chemin du champ.- Le format est construit en statique, comme pour le web, avec sa vue imprimable (
?print=1). @recit/paged(Node) sert le build, ouvre la page dans Chromium, injecte le moteur de pagination choisi, attend le signal « prêt » commun, puis appellepage.pdf().- La CLI écrit le PDF et son entrée dans
exports.json, le manifeste d'exports commun (§3.2).
Un format n'écrit que trois choses :
- son extension de schéma (dans
@recit/core, jamais dans le format) ; - sa vue imprimable (composant React rendu quand le mode est
print, qui appellewhenPrintable()de@recit/core/print) ; - sa feuille d'impression (CSS Paged Media : sauts, pages nommées propres au format).
Un format ne dépend donc pas de @recit/paged, une fondation non plus : seule la CLI (et les outils
de rendu Node) le font. C'est la règle de 03-packaging.md §8 (scope:tool), sans exception : les
tranches du toon, les PNG des cartes et les images des stories sont capturées par la CLI à travers
le runner.
3.2 Les types : tout dans @recit/core, ajoutés phase par phase
Principe : aucun champ de schéma avant la phase qui l'implémente. PDF0 ne type que ce que le
deck utilise ; chaque phase ajoute ses champs et son extension (09-langues-et-json.md L6).
Primitives sans dépendance
Pour éviter un cycle (source.ts importe déjà config/validate.ts), les briques partagées quittent
source.ts et validate.ts pour un module de base sans dépendance,
packages/core/src/config/primitives.ts, importé par les deux :
LocaleTagSchema(déplacé depuisschema/source.ts) ; leLocaleSchemaen double devalidate.tsest supprimé ;StoryFormatSchema = z.enum(STORY_FORMATS), défini une seule fois.STORY_FORMATSdevient un tupleas constetStoryFormatest dérivé du schéma (z.infer), au lieu d'être écrit à part ;LengthSchema(longueur CSS :12mm,0.5in,10pt,1080px) ;PrintThemeSchema, dérivé deThemeModeSchemasanssystem(un PDF n'a qu'un schéma de couleurs), jamais réécrit. L'API exacte de zod/mini pour cette dérivation est à vérifier à l'implémentation.
Schéma commun et extensions par format
Fichier : packages/core/src/config/validate.ts (zod/mini, comme les schémas existants), types
dérivés dans packages/core/src/config/schema.ts. Le schéma commun ne contient que ce qui vaut pour
tous les formats ; chaque extension pose ses défauts (z.prefault sur les sous-objets, règle du
CLAUDE.md : .default({}) ne lance pas le schéma interne). Il n'y a pas de table de défauts hors
schéma.
Esquisse à la fin de PDF1 (deck et report) :
TypeScript
Les noms exacts des API zod/mini (z._default, z.prefault, z.extend, z.omit) sont à vérifier
à l'implémentation ; le principe est fixe : z.omit ou z.refine pour retirer un champ, z.extend
pour en ajouter, jamais de redéfinition.
Les champs suivants n'entrent dans le schéma qu'avec leur phase :
| Phase | Schéma | Ajoute |
|---|---|---|
| PDF0 | DeckPdfOptionsSchema | theme, notes, tagged, outline, output (rien d'autre) |
| PDF1 | commun + ReportPdfOptionsSchema | page.size, page.orientation, page.margins, toc (sans numéros), en-tête et pied statiques ({title}, {author}, {date}, {page}, {pages} via les boîtes de marge de Chromium) |
| PDF2 | ToonPdfOptionsSchema | pagination: "panels" | "continuous", pageHeight: Length, maxBytes |
| PDF3 | BookPdfOptionsSchema + moteur pagedjs | engine (défaut pagedjs), page.mirror, marges inside/outside, page.bleed, jetons dynamiques {chapter} et {section}, toc.pageNumbers, chapterStart, frontMatter, footnotes |
| PDF4 | DocsPdfOptionsSchema | scope: "page" | "section", sections: string[] |
| plus tard | chaque format de la liste fermée | son extension, dans sa phase |
Contrôles portés par le schéma (z.refine), ajoutés avec les champs qu'ils concernent :
- un jeton inconnu dans un en-tête est une erreur, avec la liste des jetons valides (PDF1) ;
{chapter},{section},toc.pageNumbersetpage.bleedexigentengine: pagedjs(PDF3) ;page.margins.inside/outsideexigentpage.mirror: true, etleft/rightl'interdisent (PDF3).
Le trim de 08-formats.md §3 devient pdf.page.size du livre : un concept, un champ. Les
valeurs 5x8in et 6x9in s'écrivent en valeurs littérales, { width: 5in, height: 8in } ; elles
n'entrent pas dans l'énumération de PageSizeSchema (tranché le 24/10, question 4).
Où vivent les options, et dans quel ordre elles se fusionnent
Une seule convention (tranché le 24/10, question 4), celle de 09-langues-et-json.md §2 et du code
actuel du deck : settings d'une story est la section de son format dans recit.config, et ses
clés s'écrivent à plat dans le frontmatter. Les options PDF sont donc la clé pdf de cette section,
au même niveau que les autres réglages du format.
Collision levée (tranché le 24/10). La clé stories de recit.config était la section du site
({ dir, routePrefix, decks }, StoriesConfigSchema de packages/core/src/config/validate.ts) :
le format stories n'aurait pas pu y ranger ses réglages (duration, autoplay, aspect). La
section du site est renommée publish: { dir, routePrefix, decks, … }, et stories: porte les
réglages du format, comme deck: et motion:. C'est écrit dans 02-architecture.md §5 et
09-langues-et-json.md §2.2. Le renommage est fait dans validate.ts (24/10), sans alias : une clé
de site sous stories: échoue à la validation, avec un message qui renvoie à publish.<clé>.
| Couche | Où | Schéma |
|---|---|---|
| 1. défauts | dans l'extension du format (z._default, z.prefault) | extension du format |
| 2. projet | recit.config → deck.pdf, report.pdf, toon.pdf, book.pdf ; pour les docs, docs.pdf | extension du format |
| 3. story | frontmatter pdf: = JSON settings.pdf | extension du format |
| 4. ligne de commande | --page-size, --theme, --notes… | convertis vers l'extension du format |
Il n'y a pas de section pdf: à la racine de recit.config : une option propre à un format
(notes du deck, chapterStart du livre) se pose ainsi au niveau du projet, et config et
settings restent symétriques. resolvePdfOptions(format, layers) fusionne les entrées brutes en
profondeur de 2 à 4, puis parse une fois avec l'extension du format, qui applique la couche 1. Le
type rendu est z.output<…> : les consommateurs reçoivent des options complètes et ne reparsent
jamais.
Le choix de langue ne passe pas par ces options : il reste aux flags transverses --locale et
--all-locales de 09-langues-et-json.md §5, communs à build, export et render.
Côté sources, chaque format reçoit settings.pdf typé dans sa phase (L6) : le deck en PDF0
(DeckSettingsSourceSchema), le report en PDF1, le toon en PDF2, le livre en PDF3. Avant, leurs
sources restent en looseObject.
Le manifeste d'exports, commun à tous les types de sortie
Un seul manifeste, défini une fois dans @recit/core, pour le PDF et pour tous les autres exports
générés en CI (EPUB, images, MP4, ICS, RSS, JSON, CSV…), afin qu'aucun plan n'invente le sien. Un
export ne vient pas toujours d'une story : un bloc (::map dans une page de doc, le *.schema.json
d'un :::form) ou un outil (les skills de 14-ia-scenario.md) en produisent aussi. L'origine est
donc décrite par une union, et non par un format obligatoire :
TypeScript
- Le fichier s'appelle
exports.json. La liste deskindest réservée ici :pdf,epub,png,webp(tranches du toon,08-formats.md§2),gif(motion),svg(cartes,13-cartes.md),mp4,ics,rss,json,jsonld(12-glossaire-annuaire.md§4.2),csv,fountain(14-ia-scenario.mdD10) etskill(14-ia-scenario.md). Chaque variante n'entre dans l'union qu'avec la phase qui la produit. Ce plan ne livre quekind: "pdf", avec l'originestory. Un plan qui a besoin d'un autrekindl'ajoute à cette liste, jamais à côté. DirectiveNameSchemaest l'énumération des noms du registre des directives du cœur (règle B4 de06-roadmap.md) ; l'originetoolgagne une valeur par outil qui exporte.PdfExportOptionsetPdfExportResultde la CLI disparaissent : la variantepdfdeExportFileremplacePdfExportResult,DeckPdfOptionsremplacePdfExportOptions.- Le manifeste est publié en JSON Schema comme les autres (
schema/json-schema.ts).
Contrat d'impression commun
Défini une fois dans @recit/core, sous-export @recit/core/print (code navigateur, sans
dépendance au DOM hors garde typeof window) :
- le mode impression se lit de
?print=1pour tous les formats (?mode=printreste un alias du deck) ; window.recitReady(déclaré une fois dans undeclare global) et un événementrecit:print-ready;whenPrintable()pose le signal quand : polices chargées (document.fonts.ready), images décodées, SVG de données rendus, et, avec Paged.js, fin de la pagination. Il est appelé par chaque vue imprimable et par la route d'impression de@recit/site-stories.
Le côté Node (attendre le signal depuis Playwright) vit dans @recit/paged (§4).
3.3 Le jumeau JSON des options d'export
La même option, écrite deux fois, valide deux fois avec le même schéma. Exemple du livre, dans sa
forme multi-fichiers de 08-formats.md §3, réglages à plat (§3.2) : book.md porte les réglages du
livre et l'ordre des chapitres.
book.md :
Markdown
Jumeau JSON (book.json) :
JSON
chapters,frontetbacksont des réglages du livre, pas du PDF : ils décrivent sa structure, que lisent aussi l'édition web et l'EPUB. Le PDF s'en sert pour ses pages nommées (§3.6). Seule la mise en page imprimée va souspdf.- Un chapitre (
01-lorem.md) peut aussi avoir son jumeau01-lorem.json. Comment les scènes des chapitres s'assemblent dans laStoryrelève de08-formats.md§3 (B1), pas de ce plan :"scenes": []ne signifie ici que « pas de scène dans le fichier d'entrée ». - Le JSON Schema publié (
https://recit.dev/schema/v1/<format>.json) inclutsettings.pdf, donc l'éditeur complète et valide les options d'export dans le JSON comme dans le frontmatter. recit convertfait l'aller-retour sans perte : un test deschema/convert.ts(markdownToSource→sourceToMarkdown) couvre un frontmatterpdf:complet par format.schema/parity.tsvérifie seulement quesettings.pdfest un attribut structurel identique d'une langue à l'autre, saufoutput, qui peut varier par locale.exports.json, produit par la CLI, est lui aussi un JSON typé et publié.
3.4 Moteurs : Chromium partout, Paged.js pour le livre
- Chromium (Playwright
page.pdf), déjà dans la stack, moteur de tous les formats sauf le livre. Il gère@page(size,margin,:first,:left,:right), les pages nommées (propriétépage), les sauts (break-*), et depuis Chrome 131 les seize boîtes de marge aveccounter(page)etcounter(pages). Playwright 1.50, la version minimale du pair, livre Chromium 133 : la version minimale suffit. Appel toujours explicite :page.pdf({ preferCSSPageSize: true, printBackground: true, tagged, outline }), sansheaderTemplatenifooterTemplate(ils ne voient pas les styles de la page).taggedetoutlineexistent depuis Playwright 1.42, mais sont expérimentaux côté Chromium, et les signets sont construits à partir de l'arbre balisé :outline: trueforcetagged: true(vérifié en e2e, §5). - Paged.js (MIT,
pagedjs0.4.3), injecté comme polyfill dans la même page Playwright. On n'utilise jamaispagedjs-cli, qui embarque Puppeteer et doublerait le navigateur. Il n'apporte que ce que Chromium n'a pas :string-set(titres courants par chapitre),target-counter()(table des matières, renvois et index avec numéros de page),float: footnote, fond perdu et traits de coupe. Il n'est chargé que siengine: pagedjs, et son adoption passe par un spike go/no-go (PDF3.0). - Écartés : Vivliostyle (AGPL-3.0, CLI qui duplique le navigateur), Prince (propriétaire),
WeasyPrint (Python),
pdf-lib(aucun besoin démontré).
Décision pour le report (tranchée le 24/10, question 2) : Chromium en P6, sans numéros de page
dans la table des matières. En P6, le PDF du report a une page de titre, une table des matières
en liens, sans numéros, un en-tête courant avec le titre et un pied avec la page (les deux
possibles avec les boîtes de marge de Chromium). La table des matières numérotée arrive quand le
moteur pagedjs existe (PDF3, P8), en option du report (engine: pagedjs ajouté à
ReportPdfOptionsSchema à ce moment-là). Cela évite d'adopter en P6 une dépendance figée depuis
2023 pour un seul numéro par ligne. Conséquence : 08-formats.md §1 et R2 sont amendés (fait, voir
« Corrections »).
@recit/paged isole le moteur derrière une interface, pour pouvoir remplacer Paged.js (par sa
réécriture, ou par le natif de Chromium quand target-counter y arrivera) sans toucher aux formats
:
TypeScript
Les types Playwright viennent de playwright-core (import type), jamais réécrits à la main ;
PrintEngine est un type interne de @recit/paged, pas un type partagé entre packages.
@recit/paged génère aussi la feuille @page à partir des options (pageCss(options)) : taille,
marges, miroir, boîtes de marge et jetons. Il l'injecte depuis Node (addStyleTag). Les formats
n'écrivent jamais de @page à la main pour ces réglages, seulement pour leurs pages nommées
propres.
Migration du deck. Sa vue imprimable émet @page { size: <w>px <h>px; margin: 0 } et passe à
preferCSSPageSize: true. Les goldens pris en PDF0 décident s'il y a régression. Le
@page { margin: 0 } global quitte @recit/theme pour la feuille du deck.
3.5 Webtoon : pages coupées entre les cases (recommandé)
Trois formes possibles :
| Forme | Pour | Contre |
|---|---|---|
| A. une seule page très haute | fidèle au ruban | la limite d'implémentation d'Acrobat (ISO 32000-1, annexe C), respectée par la plupart des lecteurs, est de 14 400 unités (200 pouces, 19 200 px CSS à 96 dpi) ; un épisode la dépasse vite ; comportement de Chromium et des lecteurs non mesuré |
| B. pages de hauteur fixe, coupées entre les cases | lisible partout, imprimable | une marge vide en bas de page quand la case suivante ne tient pas |
| C. mise en page BD (planches) | objet imprimé classique | c'est un autre format (mise en page de cases sur une grille), hors de l'idée du webtoon |
Tranché (24/10, question 1) : B, pagination: "panels" par défaut. Pas de CBZ.
- Largeur de page = largeur du préréglage de plateforme du toon (préréglages de W3, réutilisés).
Hauteur =
pageHeight, indépendante des tranches : les tranches de plateforme (W3) sont coupées à hauteur fixe sans égard aux cases, alors que le PDF coupe toujours entre deux cases. Les deux découpages ne sont pas les mêmes, et c'est voulu. - Chaque case porte
break-inside: avoid: une coupe tombe toujours entre deux cases, jamais dans une bulle. - Une case plus haute que
pageHeight(case verticale pleine page, cases sans gouttière) est courante : elle déclenche un avertissement activé par défaut, et la case reçoit sa propre page, plus haute. Jamais d'erreur, jamais de coupe dans une case. - L'espace restant en bas de page prend la couleur de gouttière du toon.
- La géométrie des cases est mesurée avant
page.pdf()(positions dans le DOM), avec le module partagé « lancer Chromium, attendre prêt, mesurer » de@recit/paged(§4), que les tranches de W3 utilisent aussi. - Poids. Chromium (Skia) recopie le JPEG tel quel mais réencode WebP, AVIF et PNG en Flate : un
épisode de cases WebP peut produire un PDF très lourd. La vue imprimable utilise donc des
variantes JPEG des cases, et
maxBytesfixe un budget de poids par PDF (avertissement, erreur avec--strict), vérifié en e2e surexamples/toon-episode. pagination: "continuous"(forme A) n'est promis qu'après un spike mesuré (PDF2.3) : une page de 20 000 px puis de 50 000 px, ouvertes dans pdf.js et Acrobat. Au-delà de ce qui est mesuré : erreur, hauteur mesurée, jamais devinée.toon.direction: rtl(manga) ne change pas l'ordre vertical des cases. Il change l'ordre de lecture des bulles dans une case, ce que la vue imprimable respecte déjà. Le réglage du lecteur PDF (/Direction R2L) demanderait un post-traitement : hors de la première tranche.- Chaque case porte un
alt(description) dans le schéma detoon; sans lui, le PDF balisé est muet. Unaltmanquant est un avertissement, pas une erreur (tranché le 24/10, question 6). EPUB et CBZ pour toon : hors périmètre (le défilement vertical continu n'est pas normalisé en EPUB).
3.6 Book : chapitres, table des matières, notes, renvois
Le livre est le seul format qui exige Paged.js (engine: pagedjs par défaut), sous réserve du spike
PDF3.0.
- Chapitres. Un chapitre de
chapters= une section avecbreak-before: rightsichapterStart: right(page blanche insérée au besoin), page nomméechapterdont la première page n'a pas de titre courant (@page chapter:first). - Pré et post-texte. Les réglages
frontetbackdu livre (§3.3) donnent des pages nommées (front,back).frontMatter.numbering: romandonnecounter(page, lower-roman)surfrontet remet le compteur à 1 au premier chapitre. - Titres courants.
{title}et{chapter}viastring-setsur le titre de chapitre. Par défaut : titre du livre à gauche, chapitre à droite (@page :leftet:right), inversé en RTL. Paged.js écrit ces titres comme du texte réel dans chaque page : ils doivent être marquésaria-hidden(artefacts) pour ne pas être lus comme du contenu (vérifié en PDF3.6). - Table des matières. Générée depuis la Story (titres jusqu'à
toc.depth), en liens, avectarget-counter(attr(href), page). Les signets PDF (outline) viennent des mêmes titres. - Notes. La syntaxe GFM
[^1]existe déjà dans la Story.footnotes: page→float: footnote(Paged.js),chapter-endetbook-end→ liste de notes en fin de chapitre ou de livre, sans Paged.js. - Renvois et index.
:index[terme]est déjà prévu en B2 (08-formats.md§3) ; une directive de renvoi (« page N » en PDF, lien en web) est à fixer en B2 avec12-glossaire-annuaire.md. Ce plan ne fixe aucune syntaxe : il fournit seulementtarget-countervia le moteurpagedjs. Une cible absente est une erreur de build. - HTML intermédiaire.
@recit/bookproduit une fois le HTML du livre (la Story rendue en hast,hast-util-to-htmlest déjà dans la stack). La chaîne PDF le consomme. L'EPUB (sérialisation XHTML,nav.xhtml, OPF, zip,epubcheck) reste entièrement en B3, hors de ce plan. - Impression professionnelle.
page.bleedet les traits de coupe passent par Paged.js. CMJN, OutputIntent et PDF/X : hors périmètre (Chromium ne produit que du RVB).
3.7 Pages et sections de doc (Le framework de sites), en option
Le PDF des docs est optionnel et vient après le livre (PDF4). Il n'est pas un critère de P6.
- Ce qu'on exporte.
scope: "page": une page de doc.scope: "section": une section de la sidebar, ses pages dans l'ordre de navigation, avec une table des matières. Les options sont la sectiondocs.pdfderecit.config(couche 2), typées parDocsPdfOptionsSchema. Si elles doivent aussi passer par les options du plugin@recit/site-stories, ce sont les mêmes données et le même schéma, pas une cinquième couche. - Rendu : à démontrer avant de promettre.
scope: "page"est simple : on imprime la route existante avec la feuille d'impression.scope: "section"demande une route d'impression qui rend plusieurs pages MDX, alors que le cœur ne les importe que dans son module virtuel (§2.1). Le spike PDF4.0 doit prouver qu'un plugin Vite de@recit/site-stories(son propreimport.meta.globet l'ordre de navigation) suffit sans patch du cœur. Sinon : changement depackages/site, ou périmètre limité àscope: "page". - Ordre de build.
recit export --to pdf --site <dir>: (1) build du site ; (2) service du dossier de sortie et impression des routes listées ; (3) écriture des PDF et deexports.jsondirectement dans le dossier de sortie (dist/), sans second build. Le bouton litexports.jsonà l'exécution (fetch) : pas de fichier ou pas d'entrée, pas de bouton, donc jamais de lien mort sur une prévisualisation. - Le bouton. Un composant de
@recit/site-stories, dans le slotcontent-before(page) etnavigation-after(section). Les composants interactifs (playground, onglets de code) ont une variante imprimable (tous les onglets dépliés, playground masqué avec un lien).
Où se fait le rendu, pour tous les formats. En CI uniquement (07-domaines.md §5). La façon
dont les fichiers générés en CI arrivent sur Vercel (déploiement « prebuilt » depuis GitHub Actions,
ou stockage statique séparé) n'est pas une tâche de ce plan : c'est une décision de 07-domaines.md
§5, qui concerne aussi les MP4 de motion et les tranches du toon. Elle est un prérequis pour qu'un
PDF soit téléchargeable sur book.recit.dev ou webtoon.recit.dev.
Le panneau d'export. ExportPanel reste un composant de @recit/site-kit (07-domaines.md
§4), comme les autres composants de site de format. Il lit exports.json et affiche les fichiers de
chaque kind. Ce plan ne fournit que les entrées kind: "pdf" du manifeste.
3.8 Langues : un PDF par locale, RTL et CJK
--all-localesproduit un PDF par locale dei18n.locales, nommé paroutput({slug}.{locale}.pdfpar défaut). Chaque fichier est une entrée du manifeste avec salocale. La table des matières, l'index et les jetons ({date}) sont localisés (Intl).<html lang dir>est posé depuis la locale (dirdéduit comme dans09-langues-et-json.md). Les feuilles utilisent des propriétés logiques. En RTL,@page :leftet:rights'échangent pour les titres courants et les marges miroir. Le comportement de Chromium sur la progression des pages en RTL est à vérifier par un golden.- Polices. Chromium embarque des sous-ensembles des polices utilisées. Le lint de couverture des
glyphes de
09-langues-et-json.mddevient bloquant pour un PDF (un carré vide figé dans un fichier ne se corrige plus). - Plus tard, sans champ de schéma tant qu'aucune phase ne les implémente : contrôle
fsType(licence d'embarquement des polices),fonts.fallbackpar locale, seuilimages.minDpi. - Hors périmètre de la première version : écriture verticale CJK (
writing-mode) et sens de reliure RTL du lecteur PDF (/Direction).
3.9 La ligne de commande
| Commande | Effet | Phase |
|---|---|---|
recit export --to pdf <file> | aiguille sur format: de la source (Markdown ou JSON) ; erreur si le format n'a pas de PDF | PDF1 |
recit export --to pdf --site <dir> | pages et sections de doc listées dans docs.pdf | PDF4 |
recit export [file] et --html restent tels quels ; export epub (B3) et export images (W3,
11-images.md) appartiennent à leurs plans. Les flags sont convertis vers l'extension du format
puis passent par resolvePdfOptions(), comme le reste.
4. Packages et responsabilités
| Package | Rôle dans l'export PDF | Dépend de |
|---|---|---|
@recit/core | tous les types : primitives, PdfOptionsSchema et ses extensions, resolvePdfOptions(), ExportManifestSchema ; sous-export print (navigateur) : ?print=1, recitReady, événement, whenPrintable() ; JSON Schemas publiés | zod (existant) |
@recit/paged (packages/tools/paged, scope:tool), Node | la chaîne : sous-export @recit/paged/runner, seul module « lancer Chromium, servir un build, attendre prêt, mesurer, imprimer ou capturer », utilisé par le PDF, les tranches du toon (W3), les images (11-images.md) et les cartes (13-cartes.md) ; PrintEngine, pageCss(options) ; injection de Paged.js depuis Node. Importé par la CLI seulement | @recit/core ; Playwright en pair optionnelle ; pagedjs chargé à la demande |
@recit/theme | print.css réduit à une base commune (animations coupées, print-color-adjust, pre-wrap), commentaires en anglais | — |
Formats (@recit/deck, report, toon, book) | vue imprimable + feuille d'impression du format (pages nommées, sauts). Les règles d'impression du deck quittent @recit/theme pour @recit/deck. @recit/book produit le HTML intermédiaire, @recit/toon la découpe des tranches à partir de la géométrie mesurée. Aucun type d'export défini ici | @recit/core, jamais @recit/paged : pour W3, la CLI capture par @recit/paged/runner puis appelle la découpe du toon |
@recit/cli | export --to pdf, export images (W3), flags → schéma, aiguillage sur format:, écriture du PDF et du manifeste. Seul package qui importe @recit/paged. Plus de lanceur de Chromium ni de types Playwright locaux | @recit/core, @recit/paged, formats |
@recit/site-stories (PDF4, option) | bouton dans les slots, variantes imprimables des composants MDX, route d'impression si le spike la valide ; appelle whenPrintable() | @recit/site (seul package autorisé), @recit/core |
@recit/site-kit (privé) | ExportPanel unique, lit exports.json (tous les kind) | @recit/core |
Pourquoi @recit/paged/runner plutôt qu'un nouveau package : le module existe déjà en germe dans la
CLI, il porte déjà le pair Playwright, et un package de plus pour un seul module contredirait la
règle « une seule stack ». Tranché le 24/10 (question 3) : le lancement de Chromium reste dans
@recit/paged/runner, malgré un nom qui sert mal les captures d'images.
@recit/paged est Node uniquement : aucun format ne l'importe dans un bundle client, donc ni
Playwright ni node:http ne peuvent y entrer. Le code navigateur (contrat, whenPrintable()) est
dans @recit/core/print.
Dépendances :
- existantes : Playwright (passe de pair optionnelle de
@recit/clià pair optionnelle de@recit/paged, la CLI la déclarant toujours ;playwright-corepour les types) ;hast-util-to-html. - nouvelles, justifiées :
pagedjs(MIT), pour book uniquement, après le spike PDF3.0. - test seulement :
pdfjs-dist(Apache-2.0) en dépendance de dev dee2e/, exécuté dans le Chromium de Playwright, plutôt que poppler, outil système hors de la stack JS. Nécessaire dès PDF0 (goldens). - refusées :
pdf-lib(pas de besoin démontré), Vivliostyle, Puppeteer etpagedjs-cli(deuxième navigateur). - CI seulement, jamais dans un package : éventuellement veraPDF (question ouverte, Java).
epubcheckrelève de B3.
Chaque changement de package publié vient avec son plan de version (nx release plan). Aucun patch
de packages/site n'est prévu pour PDF0 à PDF3 ; pour PDF4, le spike PDF4.0 décide.
5. Tests
Unitaires (vitest), @recit/core :
- parse de chaque extension, défauts (
taggedetoutlineàtrue, moteur par format), effet dez.prefault;DeckPdfOptionsSchemarefusepage.size(test de l'omit) ; - refus explicites, avec leur phase : jeton inconnu (PDF1),
{chapter}avecengine: chromium,bleedsans Paged.js,insidesansmirror(PDF3) ; - ordre de fusion de
resolvePdfOptions()(config < story < CLI), parse unique ; - JSON Schema généré :
settings.pdfprésent pour chaque format qui exporte en PDF dès sa phase, absent pourmotionetstories; - aller-retour
markdownToSource→sourceToMarkdown(schema/convert.ts) avec un frontmatterpdf:complet ;parity.ts:settings.pdfidentique entre langues, saufoutput; ExportManifestSchema: aller-retour d'un manifeste produit par la CLI.
Unitaires, @recit/paged : pageCss(options) en snapshot (taille, marges, boîtes de marge,
RTL) ; sélection du moteur.
e2e (Playwright, fixtures examples/*, Lorem Ipsum) : chaque PDF exporté est ouvert avec
pdfjs-dist dans le même Chromium, puis :
- nombre et taille des pages égaux au golden de la fixture (deck : nombre de slides, plus les pages de notes) ;
- signets : titres et ordre égaux aux titres de la Story, avec
outline: trueettaggedfalsepuistrue(pour vérifier queoutlineforcetagged) ; - balisage : PDF marqué et arbre de structure non vide,
Langégal à la locale ; pour le livre, les titres courants et numéros de Paged.js absents de l'arbre (artefacts) ; - polices : aucune police non embarquée ;
- métadonnées : titre et auteur égaux à
meta; - liens : chaque entrée de la table des matières pointe vers la page attendue ; pour le livre, le numéro imprimé correspond à cette page ;
- régression visuelle : chaque page rendue en PNG et comparée par
toHaveScreenshot, comme les captures existantes (e2e/__screenshots__) ; - toon : aucune case coupée (géométrie vérifiée dans le DOM avant impression), largeur égale au
préréglage, poids sous
maxBytes; - docs (PDF4) : le bouton n'apparaît que si
exports.jsona une entrée, et son lien répond ; - i18n : un fichier par locale avec
--all-locales, et un golden RTL.
CI : version de Playwright épinglée, pour que les goldens ne bougent pas avec Chromium.
6. Phases
Les phases s'appellent PDF0 à PDF5. Elles ne créent pas de nouvelle phase de roadmap : elles se logent dans P6, P7 et P8, sauf PDF4 (docs, optionnel) et PDF5.
PDF0 — Le deck, typé (S), au début de P6
- PDF0.1 —
config/primitives.ts:LocaleTagSchemadéplacé,LocaleSchemaen double supprimé,StoryFormatSchemaetStoryFormatdérivés deSTORY_FORMATS,LengthSchema,PrintThemeSchemadérivé deThemeModeSchema. - PDF0.2 —
DeckPdfOptionsSchema(theme,notes,tagged,outline,output),resolvePdfOptions(),ExportManifestSchemaavec la seule variantepdf, types dérivés. - PDF0.3 —
deck.pdfdansDeckConfigSchema,settings.pdfdansDeckSettingsSourceSchema; JSON Schemas régénérés ; test d'aller-retour deconvert.ts. - PDF0.4a —
pdfjs-disten dépendance de dev dee2e/(question 7 tranchée avant cette tâche), lecture du nombre, de la taille et du rendu PNG des pages d'un PDF. - PDF0.4 — goldens du PDF actuel du deck (pages en PNG, nombre et taille des pages) avant toute migration.
- PDF0.5 —
PdfExportOptionsetPdfExportResultde la CLI remplacés parDeckPdfOptionsetExportFile;tagged: trueetoutline: truepassés à l'appelpage.pdfexistant.
Sortie : le PDF du deck est balisé, a des signets, et ses options sont typées par @recit/core,
identiques dans le frontmatter et dans le JSON.
PDF1 — La chaîne dans @recit/paged, deck et report (M), P6 (R2, R4)
- PDF1.0 — déplacer le stub
packages/formats/pagedverspackages/tools/paged(tagscope:tool) ; nouvelle description dupackage.json: « Node print chain used by the CLI » ; mettre à jourNOTICE.md(« What comes from where ») et03-packaging.md§2. - PDF1.1 —
loadPlaywright,serveStaticet l'attente « prêt » quittent la CLI pour@recit/paged/runner; module partagé de rendu, de mesure et de capture. - PDF1.2 — contrat d'impression et
whenPrintable()dans@recit/core/print, le deck s'y branche sans changement visible. - PDF1.3 — interface
PrintEngine, moteurchromium,pageCss(options);page.pdfavecpreferCSSPageSize: true,tagged,outline; replichannel: "chrome"retiré, ou vérificationbrowser.version() >= 131avec une erreur claire quand les boîtes de marge sont requises ; e2eoutlineavectaggedfaux et vrai. - PDF1.4 —
print.cssdécoupé : base commune dans@recit/theme, règles du deck dans@recit/deck, commentaires en anglais. - PDF1.5 — migration du deck sur
@recit/paged, comparée aux goldens de PDF0. - PDF1.6 — schéma commun (
page,tocsans numéros, en-têtes statiques) etReportPdfOptionsSchema;report.pdfen config etsettings.pdfdu report. - PDF1.7 — vue imprimable de
report: page de titre, table des matières en liens sans numéros, en-tête avec le titre et pied avec la page (boîtes de marge), tableaux à en-tête répété. - PDF1.8 — CLI :
recit export --to pdf <file>avec aiguillage surformat:, écriture deexports.json. - PDF1.9 — e2e : pages, signets, balisage, polices, métadonnées, PNG, pour deck et report.
Sortie : c'est le critère de P6 déjà écrit (« l'export PDF du deck passe par @recit/paged sans
régression »), plus un PDF de examples/report-quarterly balisé, avec signets, vérifié en e2e.
PDF2 — Webtoon (M), dans P7
- PDF2.1 —
ToonPdfOptionsSchema(pagination,pageHeight,maxBytes),toon.pdfen config etsettings.pdfdu toon ;altdes cases, unaltmanquant donne un avertissement. - PDF2.2 — vue imprimable : pages de la largeur du préréglage (W3),
break-inside: avoidpar case,pageHeightindépendante des tranches, case trop haute sur sa propre page avec un avertissement, variantes JPEG des cases, mesure par le module partagé. - PDF2.3 — spike mesuré de
pagination: continuous(pages de 20 000 et 50 000 px dans pdf.js et Acrobat) avant de le promettre. - PDF2.4 — e2e : aucune case coupée, PNG des pages, budget de poids sur
examples/toon-episode, deux langues.
Sortie : examples/toon-episode produit un PDF par langue, sans case coupée, sous son budget de
poids, listé dans exports.json. À ajouter aux critères de P7.
PDF3 — Book (L), dans P8 (B2)
- PDF3.0 — spike go/no-go Paged.js sur
examples/book-novella: garder 0.4.3, adopter la 0.5.0-beta, ou attendre le natif de Chromium. Décision écrite avant la suite. - PDF3.1 —
BookPdfOptionsSchema,book.pdfen config etsettings.pdfdu livre ; réglages du livre à plat (trimremplacé parpdf.page.sizedans08-formats.md). - PDF3.2 — moteur
pagedjsderrièrePrintEngine, ajout depagedjsselon PDF3.0 ; optionengine: pagedjsouverte au report (table des matières numérotée). - PDF3.3 — chapitres sur page impaire, pages nommées
front,chapter,back, numérotation romaine puis arabe, marges miroir. - PDF3.4 — titres courants, table des matières numérotée, notes de bas de page,
target-counterpour l'index et les renvois de B2. - PDF3.5 — la chaîne PDF consomme le HTML intermédiaire produit par
@recit/book(l'EPUB reste en B3). - PDF3.6 — un PDF par langue, golden RTL, lint de glyphes bloquant ; contenu des boîtes de marge de Paged.js marqué comme artefact.
- PDF3.7 — e2e : numéros de la table des matières égaux aux pages réelles, notes, signets.
Sortie : c'est le critère de P8 déjà écrit (examples/book-novella produit un PDF imprimable,
un EPUB valide et une édition web), précisé pour le PDF : balisé, signets, numéros de page de la
table des matières vérifiés, un fichier par langue, listé dans exports.json.
PDF4 — Pages et sections de doc (M), optionnel, après P8
- PDF4.0 — spike : une route d'impression charge les MDX d'une section par un plugin Vite de
@recit/site-stories(glob propre et ordre de navigation), sans patch du cœur. Sinon, changement depackages/siteouscope: "page"seulement. - PDF4.1 —
DocsPdfOptionsSchema,docs.pdf;recit export --to pdf --site(build, impression, écriture dansdist/). - PDF4.2 — variantes imprimables des composants MDX ; bouton dans les slots, piloté par
exports.jsonlu à l'exécution.
Sortie : une page (et, si le spike le permet, une section) de docs.recit.dev se télécharge en PDF, généré en CI, sans Chromium sur Vercel.
PDF5 — Les autres formats, dans leur phase
Chaque format de la liste fermée du §1 ajoute son extension de schéma et sa vue imprimable dans sa propre phase, par le même chemin, sans chaîne nouvelle.
Corrections à apporter aux autres plans (par l'intégrateur)
Appliquées le 24/10/2026 à 03-packaging.md, 06-roadmap.md, 07-domaines.md, 08-formats.md et
aux plans 10 à 14. Les corrections de 02-architecture.md, 09-langues-et-json.md et NOTICE.md
attendent la fin de P3 (fichiers en cours de modification). La décision de 07-domaines.md §5
(Vercel) reste ouverte (question 8).
08-formats.md§0 : ajouter « PDF » aux exports detoon; colonne « Utilisé par » de@recit/paged: deck, report, toon, book, puis les formats suivants ; remplacer « tous → PDF » par un renvoi à la liste fermée de ce plan.08-formats.md§1 et R2 : en P6, le PDF du report a une table des matières sans numéros ; Paged.js et la table numérotée arrivent en P8 (PDF3), en option. Réglages du report à plat (paper→pdf.page.size,toc→pdf.toc), sans cléreport:.08-formats.md§2 et W3 : la capture des tranches passe par@recit/paged/runner; le PDF du toon a son propre découpage, distinct des tranches.08-formats.md§3, B2 et B3 : réglages du livre à plat, sans clébook:(chapters,front,backdanssettings) ;trim→pdf.page.size; le HTML intermédiaire est produit par@recit/booket partagé avec la chaîne PDF ; l'EPUB reste en B3.03-packaging.md§6 :@recit/pagedest Node uniquement, avec le sous-exportrunner;@recit/coregagne le sous-exportprint.06-roadmap.md: critères de sortie de P6 (PDF0, PDF1), P7 (PDF2) et P8 (PDF3) ; PDF4 optionnel après P8.07-domaines.md§4 :ExportPanellitexports.json(ExportManifestSchemade@recit/core). §5 : décision sur la façon dont les fichiers générés en CI arrivent sur Vercel (PDF, EPUB, MP4, tranches).10-formulaires.md,12-glossaire-annuaire.md,13-cartes.mdet14-ia-scenario.md:ExportPanelrenvoie au composant de@recit/site-kit, et leurs sorties (JSON, CSV, PNG, MP4, ICS, Fountain…) deviennent des varianteskinddeExportManifestSchema, ajoutées dans leur phase.11-images.md(l. 55 et captureslocator.screenshot()) et13-cartes.md(PNG) : la capture passe par@recit/paged/runner, qui porte le pair Playwright.09-langues-et-json.md: rappeler que--localeet--all-localespilotent aussiexport --to pdf.- Description de
@recit/paged(package.json) : « Shared print pipeline and PDF output of every Récit format that prints ».
7. Risques
| Risque | Parade |
|---|---|
| Paged.js figé : 0.4.3 (juillet 2023), 0.5.0-beta.2 (octobre 2024), aucune version stable depuis ; réécriture en cours | interface PrintEngine ; Paged.js réservé à book (et en option au report) ; spike go/no-go PDF3.0 ; Chromium seul pour tous les autres |
Paged.js lent ou fragile sur les longs documents (08-formats.md §9) | goldens sur examples/book-novella ; délai d'attente réglable ; mesure du temps en CI |
| Le rendu change avec la version de Chromium, les goldens cassent | Playwright épinglé en CI ; mise à jour des goldens dans un commit dédié |
Migration du deck vers preferCSSPageSize: true avec des différences au pixel près | goldens pris en PDF0, avec pdfjs-dist, avant la migration |
tagged et outline expérimentaux dans Chromium | e2e dédiés ; outline force tagged |
| PDF balisé de mauvaise qualité (dépend du HTML), et plus faible pour le livre (titres courants et paragraphes coupés par Paged.js) | HTML sémantique, alt recommandé, ordre DOM = ordre de lecture ; artefacts marqués ; qualité de balisage moindre acceptée pour le livre |
| PDF du webtoon trop lourd (WebP, AVIF, PNG réencodés en Flate) | variantes JPEG des cases ; maxBytes ; e2e de poids |
| Case de webtoon plus haute qu'une page | page propre à la case, avertissement ; jamais de coupe |
| Route d'impression des docs impossible sans patch du cœur | spike PDF4.0 ; PDF4 optionnel ; repli sur scope: "page" |
| Fichiers générés en CI absents sur Vercel, boutons morts | bouton et panneau pilotés par exports.json ; décision de déploiement dans 07-domaines.md §5 |
| Dérive vers l'impression professionnelle (CMJN, PDF/X) | hors périmètre écrit ; bleed seulement avec Paged.js |
| Chaque format redéfinit ses options, son manifeste ou son signal « prêt » | schémas, manifeste et contrat dans @recit/core ; revue : aucun type d'export hors de @recit/core |
| Temps de CI (un PDF par locale et par format) | nx affected ; PDF des sites seulement sur la branche principale et les releases |
8. Questions ouvertes
Les questions 1 à 6 ont été tranchées le 24/10/2026, sans l'utilisateur et à sa demande (décisions réversibles). Les questions 7 à 9, et veraPDF dans la question 6, restent ouvertes.
Webtoon : la forme B est-elle validée ? Faut-il un jour un CBZ ?Tranché (24/10) : forme B (pages de hauteur fixe coupées entre les cases, case trop haute sur sa propre page) ; pas de CBZ.Report : table des matières sans numéros en P6, numérotée en P8 avec Paged.js ?Tranché (24/10) : oui, sans numéros en P6, numérotée en P8 (Paged.js).Nom du runner : garderTranché (24/10) : le lancement de Chromium reste dans@recit/paged/runner, ou un package nommé pour ce rôle ?@recit/paged/runner.Réglages à plat : une seule convention (Tranché (24/10) : une seule convention de réglages à plat ; les tailles s'écrivent en valeurs littérales, hors de l'énumération.settings= section du format), sans clésbook:nireport:? Les tailles5x8inet6x9indans l'énumération ?Liste fermée et roadmap : un PDF pourTranché (24/10) : pas de PDF pourstories? Le PDF des docs optionnel, après P8 ?stories; PDF des pages de doc optionnel, après P8.Accessibilité : le PDF balisé est-il une obligation, et unTranché (24/10) : PDF balisé activé par défaut ; unaltmanquant fait-il échouer le build ?altmanquant est un avertissement. Reste ouvert : veraPDF en CI (Java), oui ou non ?- Lecteur de PDF pour les tests :
pdfjs-disten dépendance de dev dee2e/(recommandé, à trancher avant PDF0), ou poppler en CI ? - Vercel : déploiement « prebuilt » depuis GitHub Actions pour tous les sites qui servent des
fichiers générés (PDF, EPUB, MP4, tranches), ou autre stockage statique ? (Décision à porter dans
07-domaines.md§5.) - Docs (si PDF4 est gardé) : quelles sections de docs.recit.dev méritent un PDF ? Le bouton
va-t-il dans
content-beforeou dans un menu d'actions de page ?