Fondations

02 — Architecture cible

Statut : accepté · Phase : transverse · Mis à jour : 25/10/2026.

1. Vue d'ensemble

Code
┌──────────────────────── @recit/core ────────────────────────┐ story.md / .mdx ──► │ scan ─► frontmatter ─► split en scènes ─► directives ─► AST │ recit.config.ts ──► │ defineConfig / mergeConfig ─► thème normalisé (tokens HSL) │ │ rules (densité, accessibilité) ─► diagnostics │ └──────────────────────────────┬──────────────────────────────┘ │ Story { meta, settings, scenes[], diagnostics } ┌──────────────┬──────────────┬───────────────┼──────────────┬──────────────┬──────────────┐ ▼ ▼ ▼ ▼ ▼ ▼ │ @recit/deck @recit/motion @recit/site-stories @recit/report @recit/toon @recit/book │ slides, Story ─► routes, sections, cases, bulles, chapitres, notes, │ présentateur handover ─► embeds MDX, :value, lecteur scroll, renvois, index │ HTML / PDF Remotion MP4 Pagefind ::chart tranches, PDF PDF / EPUB / web │ │ │ │ │ └──────────── PDF, par la CLI ─────────────────┴──────────────────────────────┘ │ ▼ │ @recit/paged (Node, CLI seulement : runner Chromium, Paged.js pour book) │ @recit/data (sources, agrégations, graphiques SVG) ◄───────┘ @recit/theme (tokens d'une seule teinte, CSS de chaque format, thèmes motion et lettrage) @recit/cli : recit dev | build | export [--to pdf|html|epub|images|ics|rss] | render | convert | validate | mcp (liste unique : 03-packaging.md §6)

2. Le modèle Story

On généralise CompiledDeck (presentation-engine/packages/core/src/types). Les champs propres à un format sont sous une clé dédiée. Le cœur ne connaît aucun format.

TypeScriptTypeScript
type StoryFormat = | "deck" | "motion" | "docs" | "report" | "toon" | "book" | "roadmap" | "calendar" | "stories"; // specs : 05-video.md, 08-formats.md ; puis glossary, directory, atlas dans leur phase (règle B1) type Story = { version: 1; format: StoryFormat; // frontmatter `format:`, défaut "deck" meta: { title: string; description?: string; author?: string; lang: string }; settings: ResolvedConfig; // recit.config.ts + frontmatter, après mergeConfig design: { width: number; height: number; aspectRatio: string }; themeCss: string; scenes: Scene[]; diagnostics: Diagnostic[]; summary: { totalDurationMs: number; formattedTotalDuration: string }; }; type Scene = { id: string; // stable : `id=` explicite, sinon s1, s2… (comme le handover emaki) index: number; source: { file: string; line: number; endLine: number }; // pour les erreurs et la réécriture attrs: Record<string, string>; // attributs du séparateur : layout, type, hold, dur, cols… html: string; // rendu HTML sanitizé (deck, docs) mdast?: Root; // conservé pour les formats qui réinterprètent (motion) steps: number; notesHtml?: string; durationMs?: number; density: DensityReport; accessibility: A11yReport; };
  • compileDeck devient compileStory. compileDeck reste un alias pendant la migration.
  • Le deck utilise scene.html, steps et notesHtml, comme aujourd'hui.
  • Le motion relit scene.mdast et scene.attrs pour produire un handover emaki (section 4).
  • Les composants MDX restent compilés côté CLI ou Vite, un module par scène (<file>.recit-scene-N.jsx, ex-presentation-slide-N.jsx, voir 6.2).

2 bis. Entrées : Markdown, JSON et langues

compileStory accepte un .md/.mdx ou un .json typé (StorySource), complété par des compagnons JSON et des variantes de langue (example.fr.md, example.fr.json). Le Markdown est d'abord converti en StorySource, puis tout suit le même chemin. La spécification complète (schémas zod, JSON Schema publiés, fusion par id de scène, résolution des langues et repli) est dans 09-langues-et-json.md.

Code
example.md ─┐ example.json (sidecar) ─┐ example.fr.md ─► markdownToSource ─► StorySource ◄─ fusion ◄─ example.fr.json example.json (story) ────────────────┘ │ ▼ compileStory(source, { locale }) ─► Story ─► format

3. Syntaxe commune

La syntaxe de presentation-engine est la référence. Elle est déjà documentée dans presentation-engine/docs/pages/documentation/writing.

MarkdownMarkdown
--- title: Lorem ipsum format: deck # deck | motion | docs… aspectRatio: 16:9 lang: en --- --- layout=cover id=open # Lorem ipsum ### Dolor sit amet :::notes Speaker notes. ::: --- layout=grid cols=3 :::card ### Lorem :::
ÉlémentSyntaxe
Métadonnées du récitfrontmatter YAML
Séparateur de scène--- seul, ou --- clé=valeur … (attributs de scène)
Blocsdirectives :::card, :::callout, :::step, :::left / :::right, :::col, :::code-group, :::component{name=…}, :::notes
Inline:badge[…]{type=…}, ::image{src alt fit}, :kbd[…]
MDX.mdx : imports ESM et composants React

Registre des directives (règle B4, 06-roadmap.md). Le cœur ne garde que les noms des directives et des attributs réservés. Le plugin de chaque bloc vit dans la fondation qui le porte : ::chart dans @recit/data, ::map dans @recit/geo, :term (P14, plan 12) et le bloc ```pixel dans @recit/components. Attributs réservés : type=, entry=, end, prompt=, cast=, beat=, set=, shot=, pose=, expression=, camera=, background (jamais bg). Le mot kind est réservé au discriminant des documents JSON (09-langues-et-json.md §2.2) : aucun réglage ni attribut ne s'appelle kind. Les réglages du frontmatter sont à plat (08-formats.md) ; la forme imbriquée n'existe que dans recit.config.

Décision à valider : les directives de scène d'emaki. emaki/PLAN.md (phase 4) propose <!-- type: stat, id: proof, hold: 1 -->, invisible sur GitHub. Récit retient les attributs du séparateur (--- type=stat id=proof hold=1) comme forme canonique. Le commentaire HTML est accepté comme alias en première ligne de scène, et le parseur fusionne les deux. Une seule forme est émise lors de la réécriture : la canonique.

4. Pont deck ↔ motion

Une même source peut être rendue en deck et en vidéo (recit build et recit render). On passe d'un layout de scène à un bloc emaki par un tableau fermé. Une forme sans correspondance produit une erreur avec numéro de ligne, jamais une devinette, selon la règle emaki « aucune métrique inventée ».

Scène RécitBloc emakiProps
layout=cover (H1 et H3 ou paragraphe)titleH1 → text, sous-titre → kicker
layout=section (H1 ou H2)chaptertitre → title, number via attribut ou préfixe 01 ·
layout=quote, ou paragraphe seulstatementtexte → text, gras → emphasis[]
titre et liste à puceslisttitre → title, puces → items[]
titre et tableau label | before | aftercompare-barslignes → rows[], unit via attribut
type=stat explicitestatH1 → value, paragraphe → label, italique → caption
type=ui-mock / type=ui-sceneidembloc ```yaml = props
tout bloc ```yaml ou ```json avec type=typeprops du bloc (échappatoire universelle)
grid, split, image-*, code, Mermaid, KaTeX, composant MDX—erreur en v1 (motion: layout "grid" has no block). Rendu en image fixe en v2 (section 5.3 de 05-video.md)

:::notes devient la voix off ou le sous-titre de la scène (champ à ajouter au schéma emaki). Les :::step deviennent des temps d'apparition (hold).

5. Configuration : un seul recit.config.ts

recit.config.ts est le seul fichier de configuration d'un projet Récit. Il remplace :

  • recit.config.ts / recit.config.tsx (configuration du site du framework de sites) ;
  • presentation.config.ts (configuration des decks de presentation-engine).

Un projet sans site (un deck seul, lancé avec recit dev slides.md) n'y met que les sections de format. Un site y ajoute les options du framework de sites habituelles, au même niveau.

TypeScriptTypeScript
import { defineConfig } from "recit/config"; // réexporte @recit/core/config export default defineConfig({ // — options du framework de sites (ex-recit.config.ts), facultatives — site: { title: "Lorem", logo: { src: { light: "/logo-light.svg", dark: "/logo-dark.svg" } } }, navigation: [ /* … */ ], redirects: [ /* … */ ], // — options Récit communes — theme: { colors: { primary: "#7c3aed" }, radius: "0.75rem", font: { /* … */ }, }, footer: { left: "{title}", right: "{page}/{total}", hideOn: ["cover"] }, components: "./components/index.ts", publish: { dir: "./stories", routePrefix: "/present" }, // récits publiés par le site (ex-`stories:`) // — options propres à chaque format, facultatives — deck: { transition: "slide", presenter: true }, motion: { theme: "warm-editorial", wordsPerSecond: 2.6, aspect: "9:16" }, report: { pdf: { page: { size: "A4" } } }, calendar: { timezone: "Europe/Paris" }, // stories: { … }, réglages du format stories (la section du site s'appelle `publish:`) // terms: { … }, glossaire partagé, à la phase du plan 12 (P14) // — langues : fonctionnalité optionnelle (09-langues-et-json.md §4) — // i18n: { defaultLocale: "en", locales: ["en", "fr", "ja", "ar", "sw"] }, });

Comment ça marche :

  • defineConfig valide tout le fichier avec zod 4 et sépare les deux parties :
    • les options du framework de sites sont passées telles quelles au chargeur du framework de sites. Elles sont validées par le framework de sites lui-même, qui retire les clés inconnues : les sections Récit doivent donc être extraites avant ;
    • les sections Récit configurent recitPlugin et recitVitePlugin, qui sont injectés automatiquement dans plugins. Plus de double déclaration (plugin Vite + plugin le framework de sites) : cela remplace le patch « clé stories » prévu en 6.2.
  • Chargement : le cœur ne connaît que recit.config.* (liste en dur dans packages/site/src/config/loader.ts). La liste contient recit.config.{ts,tsx,js,jsx,mjs}.
  • Compatibilité : un recit.config.ts existant continue de fonctionner, avec un avertissement qui invite à le renommer. presentation.config.ts est lu avec le même avertissement, jusqu'à la 0.2.
  • recit dev | build (CLI) lit le même fichier qu'un site : un deck a le même rendu en CLI et dans la doc.
  • Le schéma est en zod 4 (catalogue 4.6.5, zod/mini), fait en P3 dans packages/core/src/config/validate.ts (presentation-engine validait à la main). Le JSON Schema de recit.config est publié avec les autres (recit.dev/schema/v1/config.json).
  • publish: et stories: (tranché le 24/10) : la section du site qui publie les récits s'appelle publish: { dir, routePrefix, decks, layout, components, loadDeckConfig } (ex-stories: ; decks liste les récits hors de dir, comme aujourd'hui). La clé stories: est libre pour les réglages du format stories, comme deck: ou motion: pour les leurs : la règle « settings d'un récit = section de son format » vaut pour tous les formats (09-langues-et-json.md §2.2, 15-export-pdf.md §3.2). Le renommage est fait dans le code le 24/10 (PublishConfigSchema dans packages/core/src/config/validate.ts), sans alias : une clé de site sous stories: échoue à la validation, avec un message qui renvoie à publish.<clé>.
  • Toutes les sections sont facultatives. Sans section de format, ce sont les valeurs par défaut. Sans i18n, le projet est monolingue (voir 09-langues-et-json.md §4).

6. Intégration au le framework de sites

6.1 Niveau 1 : plugin (aucun patch du cœur)

C'est ce que fait déjà @presentation/site-extension sur le framework de sites 0.86 :

  • recitVitePlugin({ dir, decks }) : module virtuel virtual:recit-stories, une compilation par récit, des modules MDX par scène, et ?raw des sources.
  • recitPlugin({ routePrefix: "/present", layout: "none", components }) :
    • getRoutes() déclare une route par récit ;
    • getMdxComponents() expose <StoryEmbed slug slide /> (ex-PresentationEmbed) et <StoryGallery /> (ex-PresentationGallery).
  • Recherche : sous-résultats Pagefind #slide-N, contenu marqué data-pagefind-body.
  • CSS : limitée par postcss à .recit-wrapper (ex-.site-presentation-wrapper). On ajoute en tête la déclaration d'ordre des layers @layer properties; @layer theme, base, components, utilities;. À conserver, c'est ce qui a réglé le conflit de styles avec le framework de sites.
  • Hydratation : rendu client seulement, via useSyncExternalStore. C'est ce qui a corrigé l'erreur React #418.

À adapter au passage de 0.86 à 0.93 : API des plugins (SitePlugin, getRoutes, getMdxComponents), react-router 8.4, Vite 8.1, Shiki 4.5. Le test d'intégration examples/site-embed sert de garde-fou.

6.2 Niveau 2 : support natif (patchs du fork, plus tard)

À faire seulement après le niveau 1, et si le besoin est réel. Chaque patch est listé ici et dans UPSTREAM.md (supprimé depuis), section « Local patches », qui fait foi.

PatchPourquoiFichier du framework de sites touché
Clé de config stories: { dir, routePrefix } dans recit.config.tsx remplacée par recit.config.ts (§5) et le patch n° 3Éviter la double déclaration (plugin Vite + plugin du framework de sites)schéma de config, chargeur Vite
Type de navigation { type: "story", slug }Une entrée de sidebar avec l'icône du format et le nombre de scènesschéma navigation
Afficher le title des blocs de codeProblème connu : Le framework de sites affiche « Code » au lieu du titleplugin MDX ou Shiki
Ids de modules MDXLe plugin MDX du framework de sites intercepte nos modules de scène, d'où le contournement .jsx. Une option d'exclusion serait plus propreplugin MDX

7. Ce qui reste séparé

  • Remotion et Chrome Headless Shell (environ 93 Mo) ne sont jamais des dépendances de @recit/core, @recit/deck ou @recit/site-stories. Ils vivent uniquement dans @recit/motion-render.
  • Shiki reste côté build. L'entrée @recit/core/browser n'importe ni Shiki ni le pipeline Node (stubs optionalDependenciesPlugin).
  • Mermaid est chargé côté client, à la demande.
  • KaTeX : CSS embarquée en local, aucun CDN.