# Capitoli di un libro illustrato con vignette e sommari

> Un solo stile di titolo apre ogni capitolo; ogni titolo nomina negli attributi la sua vignetta e il suo sommario in corsivo, che l’apertura dipinge.

- Versione HTML: https://postext.dev/it/cookbook/storybook-chapter-vignettes
- Ricetta Nº 066 · Titoli e aperture · Livello 2 (Intermedio) · Output: Canvas
- Generi: Narrativa, teatro e prosa letteraria
- Richiede postext ≥ 1.25.0 · provata con 1.31.0 il 2026-10-13
- Pagine: [6](https://postext.dev/cookbook/storybook-chapter-vignettes/en/p01.webp?v=14c42f77), [7](https://postext.dev/cookbook/storybook-chapter-vignettes/en/p02.webp?v=14c42f77), [8](https://postext.dev/cookbook/storybook-chapter-vignettes/en/p03.webp?v=14c42f77), [9](https://postext.dev/cookbook/storybook-chapter-vignettes/en/p04.webp?v=14c42f77), [10](https://postext.dev/cookbook/storybook-chapter-vignettes/en/p05.webp?v=14c42f77)
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=storybook-chapter-vignettes&lang=en (.postext: https://postext.dev/cookbook/storybook-chapter-vignettes/en/storybook-chapter-vignettes.postext)
- Ultimo aggiornamento: 2026-10-10
- Altre lingue: [en](https://postext.dev/en/cookbook/storybook-chapter-vignettes.md), [es](https://postext.dev/es/cookbook/storybook-chapter-vignettes.md), [ca](https://postext.dev/ca/cookbook/storybook-chapter-vignettes.md), [de](https://postext.dev/de/cookbook/storybook-chapter-vignettes.md), [fr](https://postext.dev/fr/cookbook/storybook-chapter-vignettes.md), [pt](https://postext.dev/pt/cookbook/storybook-chapter-vignettes.md), [zh](https://postext.dev/zh/cookbook/storybook-chapter-vignettes.md), [ja](https://postext.dev/ja/cookbook/storybook-chapter-vignettes.md), [ar](https://postext.dev/ar/cookbook/storybook-chapter-vignettes.md)

## In breve

Le prime pagine di un’edizione italiana per ragazzi di Pinocchio. Ogni capitolo si apre con una sua immagine dipinta dai toni morbidi e un breve sommario di quello che succede.

## Che cosa comporrai

Le pagine d’apertura di un’edizione italiana per ragazzi de *Le avventure di Pinocchio* di Collodi, composta in Averia Serif Libre a 13 su 19 pt su una pagina di 190 × 240 mm. Un frontespizio sul verso sta di fronte al capitolo I: un acquerello delle colline toscane, dove un carretto tirato da un asino porta la legna alla bottega di un falegname, sopra il titolo in Fredericka the Great. Ogni capitolo si apre sotto una vignetta ad acquerello che sfuma nella carta (mastro Ciliegia con l’ascia alzata sul pezzo di legno parlante per il capitolo I, Geppetto con la parrucca gialla che prende il pezzo di legno per il capitolo II), poi la riga del capitolo e il sommario di Collodi in corsivo centrato. Le quattro immagini sono state generate con modelli di diffusione da un unico prompt di stile e dalla palette della pagina. Le testatine si stampano solo alle pagine 8 e 9, le due senza apertura. I folio stanno in dischi color legno, e due ciliegie chiudono il capitolo I senza didascalia né numero.

**Questa ricetta risponde a:**

- Come apro ogni capitolo di un libro per ragazzi con una sua vignetta e un sommario in corsivo?
- Come compongo illustrazioni non numerate: fregi, vignette, loghi?
- Come aggiungo a un’apertura una riga d’autore, un sommario o un attacco con capolettera?
- Come nascondo le testatine nelle aperture e nelle pagine bianche, o coloro un verso bianco con il colore della parte?

## La risposta breve

```js
// script.js, righe 44–64
// # Capitolo I {style="capitolo" vignetta="ceppo" summary="Come andò che Maestro Ciliegia…"}
// resourceId takes placeholders like a design text: '{attr.vignetta}' draws the resource the
// heading names, so every chapter shares one style.
const VIGNETTE = { width: 84, height: 56 }; // mm, centred at the head of the text block
const SUMMARY = 124; // mm: the summary's measure, narrower than the text's
const opener = { enabled: true,
  // A floor for the height the opener reserves, in whole lines, so the text under chapter I's
  // two-line summary starts on the same grid line as the text under chapter II's three lines.
  minHeight: pt(14 * LEAD),
  slot: { elements: [
    { kind: 'image', id: 'vignette', resourceId: '{attr.vignetta}',
      placement: at('container', 'top', 0, 0,
        { width: mm(VIGNETTE.width), height: mm(VIGNETTE.height) }) },
    // The chapter line 6 mm under the picture's foot, the summary 5 mm under the chapter line.
    text('chapter', '{titleText}', DISPLAY, 24, at('container', 'top-left', 0,
      VIGNETTE.height + 6, { width: mm(MEASURE) }), { ...caps(2.4), lineHeight: 1 }),
    text('summary', '{attr.summary}', TEXT, 12.5, at('#chapter', 'below',
      (MEASURE - SUMMARY) / 2, 5, { width: mm(SUMMARY) }), { italic: true, lineHeight: 1.38 }),
  ] },
};
const chapter = { id: 'capitolo', advancedDesign: opener };
```

## Ingredienti

**Insegna**

- [Aperture progettate](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato): Un titolo disegnato come una composizione libera di testo, filetti, riquadri e immagini, che riserva sopra il testo corrente l’altezza di cui ha bisogno.
- [Immagini nei progetti di pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-immagine): Loghi, foto e ornamenti collocati in intestazioni, piè di pagina, aperture e pagine di parte, dimensionati mantenendo le proporzioni.
- [Testatine e folio](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina): Spazi di intestazione e piè di pagina con segnaposto ({pageNumber}, {chapterTitle}, {title}…) impostati per recto o verso, con i titoli lunghi troncati o mandati a capo.

**Usa anche**

- [Stili di titolo](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)
- [Attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)
- [Capitoli che si aprono su un recto](https://postext.dev/it/docs/configuration-text.md#interruzione-prima-del-titolo)
- [Testo, filetti e riquadri nei progetti di pagina](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Testatine secondo il ruolo della pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)
- [Tipi di risorsa personalizzati](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)
- [Figure proprio qui](https://postext.dev/it/docs/document-format.md#inserimento-di-blocco-facoltativo-collocazione-in-linea-esplicita)
- [Figure e tabelle come risorse](https://postext.dev/it/docs/document-format.md#risorse)
- [Sillabazione e lingua del documento](https://postext.dev/it/docs/justification.md#lingue-supportate)
- [Margini speculari](https://postext.dev/it/docs/configuration-page-layout.md#margini-speculari)
- [Tavolozza di colori semantica](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori)
- [Pagine su un canvas](https://postext.dev/it/docs/configuration-programmatic-usage.md#rendere-una-pagina-come-bitmap)
- [Colore della carta](https://postext.dev/it/docs/configuration-page-layout.md#pagina)
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Testatine per sezione](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)

**La configurazione in breve**

- [`bodyText`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), [`colorPalette`](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori), [`footer`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`header`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`headingStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli), [`headings`](https://postext.dev/it/docs/configuration-text.md#titoli), [`layout`](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione), [`locale`](https://postext.dev/it/docs/configuration-text.md#sillabazione), [`page`](https://postext.dev/it/docs/configuration-page-layout.md#pagina), [`paragraphStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo), [`resourceTypes`](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione), [`registerResourceImage`](https://postext.dev/it/docs/architecture.md#superficie-dellapi), [`renderPageToCanvas`](https://postext.dev/it/docs/configuration-programmatic-usage.md#rendere-una-pagina-come-bitmap)

**Caratteri**

- Averia Serif Libre (OFL-1.1), Fredericka the Great (OFL-1.1), Quicksand (OFL-1.1)

## Preparazione

### 1 · Un solo stile di titolo, una vignetta nominata su ogni titolo

Il codice di questo passo è nella [risposta breve](https://postext.dev/it/cookbook/storybook-chapter-vignettes.md#la-risposta-breve). Il `resourceId` di un elemento immagine accetta gli stessi segnaposto di un testo del progetto grafico, quindi `'{attr.vignetta}'` disegna la risorsa che il titolo nomina, come `{attr.summary}` stampa il suo sommario. Entrambi i capitoli condividono lo stile `capitolo` e nominano la loro immagine sulla riga del titolo: `# Capitolo II {style="capitolo" vignetta="polenta" summary="…"}`. Un terzo capitolo richiede una risorsa e un titolo in più, non un altro stile ([elementi immagine](https://postext.dev/it/docs/configuration-page-layout.md#elementi-immagine)). La riga del capitolo si colloca dalla testa del contenitore, 6 mm sotto il riquadro della vignetta, e il sommario pende 5 mm sotto di essa; l’apertura riserva lo spazio fino all’ultima riga del sommario. Con `minHeight` come minimo di 14 righe, il testo del capitolo I comincia sulla stessa riga di quello del capitolo II, il cui sommario è più lungo di una riga; senza, il testo del capitolo I comincia 6,7 mm più in alto ([progetto grafico a tutta larghezza e avanzato](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato)).

### 2 · Carica i dipinti come risorse bitmap

```js
// script.js, righe 122–133
const PICTURES = { // id: [file, width, height, alt text in the book's language]
  frontespizio: ['frontespizio-1400.jpg', 1400, 1105, 'Colline toscane con cipressi e un borgo; '
    + 'un contadino guida un asinello con un carretto di legna verso la bottega di un falegname.'],
  ceppo: ['ceppo-1200.jpg', 1200, 800, 'Maestro Ciliegia, con la scure alzata sopra un pezzo di '
    + 'legno sul banco, si guarda intorno spaventato.'],
  polenta: ['polenta-1200.jpg', 1200, 800, 'Geppetto, con la parrucca gialla, riceve il pezzo di '
    + 'legno da Maestro Ciliegia; accanto, un paiolo di polenta fumante.'],
  ciliegie: ['ciliegie-600.jpg', 600, 626, 'Due ciliegie rosse su un picciolo, con una foglia.'],
};
const resources = Object.entries(PICTURES).map(([id, [fileId, w, h, altText]]) => ({
  id, typeId: 'fregio', kind: 'bitmap', createdAt: 0, updatedAt: 0, altText,
  bitmap: { fileId, format: 'jpeg', width: w, height: h } }));
```

Ogni immagine è un JPEG nella cartella `assets/` della ricetta, dichiarato alla sua dimensione in pixel: l’impaginazione legge solo quelle dimensioni, e il canvas dipinge ciò che `loadImage()` ha registrato sotto ogni id di file quando disegna la pagina. Le vignette e le ciliegie sono state generate su bianco e poi moltiplicate per il colore della carta `#fffaf0`, così i loro margini si stampano come la pagina e le immagini non hanno un bordo visibile. La tavola del frontespizio è tagliata a 190:150, la forma del suo campo, così l’immagine adattata riempie il campo senza una fascia di carta. Il paesaggio e le vignette sono dipinti da elementi immagine nei progetti grafici, le ciliegie da `::resource{id="ciliegie"}` nel Markdown.

### 3 · Fai del frontespizio uno stile di titolo

```js
// script.js, righe 68–85
const SKY = 150; // mm from the top edge: the foot of the painted field
// # Pinocchio {style="frontespizio" kicker="Le avventure di"}; subtitle and author come from
// the frontmatter. Every element hangs from the page, so the field runs off three edges.
const frontispiece = {
  id: 'frontespizio',
  footer: { elements: [] }, // no folio; the running heads already skip openers
  advancedDesign: { enabled: true, slot: { elements: [ // array order is paint order
    { kind: 'image', id: 'landscape', resourceId: 'frontespizio', // cut to the field's 190:150
      placement: at('page', 'top-left', 0, 0, { width: 'fill', height: mm(SKY) }) },
    text('kicker', '{attr.kicker}', LABEL, 11, at('page', 'top', 0, SKY + 11),
      { ...caps(2.6), fontWeight: 700 }),
    text('title', '{titleText}', DISPLAY, 70, at('page', 'top', 0, SKY + 17),
      { lineHeight: 1, color: col('bark') }),
    text('subtitle', '{subtitle}', TEXT, 15, at('page', 'top', 0, SKY + 44), { italic: true }),
    text('author', '{author}', LABEL, 9, at('page', 'top', 0, SKY + 58),
      { ...caps(2), fontWeight: 600 }),
  ] } },
};
```

Il frontespizio è il primo titolo del Markdown, `# Pinocchio {style="frontespizio" kicker="Le avventure di"}`, e il suo stile dipinge tutto quello che c’è sulla pagina: il paesaggio, alto 150 mm dal bordo superiore, poi l’occhiello dall’attributo, il titolo, e il sottotitolo e l’autore dal frontmatter. Ogni elemento pende dalla pagina, non dalla gabbia, così il dipinto raggiunge il bordo superiore ed entrambi i lati del foglio mentre il titolo stesso resta nell’unica colonna del libro. Il `footer` vuoto toglie il disco del folio che stampano le aperture dei capitoli, e `pages: 'body'` tiene già le testatine fuori dalla pagina ([stili di titolo](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)).

### 4 · Testatine sulle pagine di testo, folio nei dischi

```js
// script.js, righe 89–110
// The excerpt opens on book page 6, a verso, so the title page faces chapter I.
const continuation = { pageIndexOffset: 5, pageNumbering: { startAt: 6 } };
const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre sits off the page's centre
const head = (id, content, parity, x) => text(id, content, LABEL, 8,
  at('page', 'top', x, 12), { ...caps(1.8), fontWeight: 600, parity, pages: 'body' });
const DISC = 7.4; // mm: the folio's disc
// A fixed square with a radius of half its side is a circle; the figures sit in its middle.
// lineHeight 1.17: the baseline falls at 0.8 of the line, 0.94 em down, so Quicksand's
// figures (0.70 em tall) leave 0.23 em above and below them, and 'middle' centres that line.
const disc = (id, parity, pages, edge, x) => text(id, '{pageNumber}', LABEL, 8.5,
  at('page', edge, x, -(BOTTOM - DISC) / 2, { width: mm(DISC), height: mm(DISC) }),
  { fontWeight: 700, color: col('paper'), lineHeight: 1.17, verticalAlign: 'middle', parity,
    pages, box: { backgroundColor: col('wood'), borderRadius: mm(DISC / 2) } });
const header = { elements: [
  head('verso-title', '{title}', 'even', -SHIFT), // the book on the left-hand page
  head('recto-chapter', '{chapterTitle}', 'odd', SHIFT), // the chapter on the right
] };
const footer = { elements: [
  disc('verso-folio', 'even', 'body', 'bottom-left', OUTER),
  disc('recto-folio', 'odd', 'body', 'bottom-right', -OUTER),
  disc('opener-folio', 'all', 'opener', 'bottom', 0), // a chapter opens on either side
] };
```

`continuation` numera l’estratto a partire dalla pagina 6 del libro, un verso, così il frontespizio sta di fronte al capitolo I e il primo folio stampato è 7. `pages: 'body'` tiene le testatine fuori dal frontespizio e da entrambe le aperture, quindi si stampano solo alle pagine 8 e 9, il titolo del libro sul verso e quello del capitolo sul recto ([elementi di testo](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)). Il folio è un testo del progetto grafico in un quadrato fisso di 7,4 mm il cui raggio d’angolo è metà del lato, il che lo rende un cerchio; `verticalAlign: 'middle'` e l’interlinea calcolata nel commento vi centrano le cifre. Le cifre color carta hanno un rapporto di contrasto di 4,98:1 sul legno. Le etichette spaziate non hanno bisogno di compensazione: la spaziatura dopo l’ultima lettera di una riga si omette quando la riga è centrata.

### 5 · Chiudi il capitolo con un finalino non numerato

```js
// script.js, righe 114–118
// No caption prefix and no caption, so the cherries that close chapter I print with no
// label or number. The pictures the design slots paint are of this type too.
const resourceTypes = [{ id: 'fregio', name: 'Fregio', namePlural: 'Fregi', shortLabel: 'Fregio',
  captionPrefix: '', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal',
  defaultPlacement: { position: 'here', width: 0.16, align: 'center' } }];
```

Un tipo di risorsa stampa il suo `captionPrefix` e il numero prima della didascalia. Con un prefisso vuoto e nessuna didascalia, sotto le ciliegie non si stampa nulla; con `captionPrefix: 'Fregio'`, la stessa risorsa stampa *Fregio 1.* sotto il disegno e cresce di 10 mm in altezza ([tipi di risorsa](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)). Il `defaultPlacement` del tipo mette ogni risorsa dove sta la sua riga `::resource`, larga un sesto della giustezza e centrata, così il Markdown ha bisogno solo di `::resource{id="ciliegie"}` dopo l’ultimo paragrafo del capitolo. Le altre immagini condividono il tipo ma nessun `:ref` o `::resource` le nomina, quindi non vengono mai collocate né contate.

## La ricetta completa

Un unico file, composto a partire dalla cartella della ricetta con il testo di esempio e il kit comune del Ricettario già inclusi; costruisce la propria pagina. Per eseguirlo, mettilo in uno `<script type="module">` di una pagina vuota, oppure incollalo nel pannello JS di un nuovo pen di CodePen (come modulo). Importa postext da esm.sh: non c’è niente da installare né da compilare.

- Cartella della ricetta: https://github.com/drnachio/postext/tree/main/cookbook/storybook-chapter-vignettes

### script.js

```js
// ═══ Postext Cookbook · Nº 066 · Storybook chapters with vignettes and summaries ═══
// https://postext.dev/en/cookbook/storybook-chapter-vignettes
// Code: MIT · Text: C. Collodi, 1883 (PD, Gutenberg #52484) · Pictures: diffusion models
// Fonts: Averia Serif Libre, Fredericka the Great, Quicksand (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document (the sample is Italian in both)
const RECIPE = 'storybook-chapter-vignettes';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { // every colour in the config comes from here; the pictures were painted to it
  ink: '#2b2118', // text: a brown near-black
  paper: '#fffaf0', // a warm book paper, which the vignettes' margins were tinted to
  bark: '#6b4224', // chapter lines, running heads, the title
  wood: '#9a5f30', // the folio discs
};
// col(id): a colour linked to its palette entry, with the entry's hex beside the id.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // The defaults linked to main-color, bold among them, print in bark instead of blue.
  { id: 'main-color', name: 'bark (defaults)', value: { hex: palette.bark, model: 'hex' } },
];
// Text and summaries; the book's title and the chapter lines; running heads and folios.
const [TEXT, DISPLAY, LABEL] = ['Averia Serif Libre', 'Fredericka the Great', 'Quicksand'];
const TRIM = { width: 190, height: 240 }; // mm
const [TOP, INNER, OUTER] = [22, 22, 20]; // mm; mirrored, so the spine margin swaps sides
const MEASURE = TRIM.width - INNER - OUTER; // 148 mm
const [BODY, LEAD, LINES] = [13, 19, 29]; // pt, pt, and whole lines in the text block
const BOTTOM = TRIM.height - TOP - (LINES * LEAD * 25.4) / 72; // mm: 23.6
// Design-slot shorthands: a placement from an anchor's edge (mm), and a design text.
const at = (to, edge, x = 0, y = 0, size) => ({ anchor: { to, edge },
  offset: { x: mm(x), y: mm(y) }, ...(size && { size }) });
const text = (id, content, fontFamily, size, placement, extra = {}) => ({ kind: 'text', id,
  content, fontFamily, fontSize: pt(size), color: col('ink'), align: 'center',
  overflow: 'wrap', // a long line wraps in every slot, the running heads too
  placement, ...extra });
const caps = (tracking) => ({ textTransform: 'uppercase', letterSpacing: pt(tracking),
  color: col('bark') });

// #region answer: one opener for every chapter: the vignette the heading names, then its summary
// # Capitolo I {style="capitolo" vignetta="ceppo" summary="Come andò che Maestro Ciliegia…"}
// resourceId takes placeholders like a design text: '{attr.vignetta}' draws the resource the
// heading names, so every chapter shares one style.
const VIGNETTE = { width: 84, height: 56 }; // mm, centred at the head of the text block
const SUMMARY = 124; // mm: the summary's measure, narrower than the text's
const opener = { enabled: true,
  // A floor for the height the opener reserves, in whole lines, so the text under chapter I's
  // two-line summary starts on the same grid line as the text under chapter II's three lines.
  minHeight: pt(14 * LEAD),
  slot: { elements: [
    { kind: 'image', id: 'vignette', resourceId: '{attr.vignetta}',
      placement: at('container', 'top', 0, 0,
        { width: mm(VIGNETTE.width), height: mm(VIGNETTE.height) }) },
    // The chapter line 6 mm under the picture's foot, the summary 5 mm under the chapter line.
    text('chapter', '{titleText}', DISPLAY, 24, at('container', 'top-left', 0,
      VIGNETTE.height + 6, { width: mm(MEASURE) }), { ...caps(2.4), lineHeight: 1 }),
    text('summary', '{attr.summary}', TEXT, 12.5, at('#chapter', 'below',
      (MEASURE - SUMMARY) / 2, 5, { width: mm(SUMMARY) }), { italic: true, lineHeight: 1.38 }),
  ] },
};
const chapter = { id: 'capitolo', advancedDesign: opener };
// #endregion

// #region frontispiece: the title page as a heading style: a painted field, then the title
const SKY = 150; // mm from the top edge: the foot of the painted field
// # Pinocchio {style="frontespizio" kicker="Le avventure di"}; subtitle and author come from
// the frontmatter. Every element hangs from the page, so the field runs off three edges.
const frontispiece = {
  id: 'frontespizio',
  footer: { elements: [] }, // no folio; the running heads already skip openers
  advancedDesign: { enabled: true, slot: { elements: [ // array order is paint order
    { kind: 'image', id: 'landscape', resourceId: 'frontespizio', // cut to the field's 190:150
      placement: at('page', 'top-left', 0, 0, { width: 'fill', height: mm(SKY) }) },
    text('kicker', '{attr.kicker}', LABEL, 11, at('page', 'top', 0, SKY + 11),
      { ...caps(2.6), fontWeight: 700 }),
    text('title', '{titleText}', DISPLAY, 70, at('page', 'top', 0, SKY + 17),
      { lineHeight: 1, color: col('bark') }),
    text('subtitle', '{subtitle}', TEXT, 15, at('page', 'top', 0, SKY + 44), { italic: true }),
    text('author', '{author}', LABEL, 9, at('page', 'top', 0, SKY + 58),
      { ...caps(2), fontWeight: 600 }),
  ] } },
};
// #endregion

// #region folios: running heads on body pages only, folios in wood-coloured discs
// The excerpt opens on book page 6, a verso, so the title page faces chapter I.
const continuation = { pageIndexOffset: 5, pageNumbering: { startAt: 6 } };
const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre sits off the page's centre
const head = (id, content, parity, x) => text(id, content, LABEL, 8,
  at('page', 'top', x, 12), { ...caps(1.8), fontWeight: 600, parity, pages: 'body' });
const DISC = 7.4; // mm: the folio's disc
// A fixed square with a radius of half its side is a circle; the figures sit in its middle.
// lineHeight 1.17: the baseline falls at 0.8 of the line, 0.94 em down, so Quicksand's
// figures (0.70 em tall) leave 0.23 em above and below them, and 'middle' centres that line.
const disc = (id, parity, pages, edge, x) => text(id, '{pageNumber}', LABEL, 8.5,
  at('page', edge, x, -(BOTTOM - DISC) / 2, { width: mm(DISC), height: mm(DISC) }),
  { fontWeight: 700, color: col('paper'), lineHeight: 1.17, verticalAlign: 'middle', parity,
    pages, box: { backgroundColor: col('wood'), borderRadius: mm(DISC / 2) } });
const header = { elements: [
  head('verso-title', '{title}', 'even', -SHIFT), // the book on the left-hand page
  head('recto-chapter', '{chapterTitle}', 'odd', SHIFT), // the chapter on the right
] };
const footer = { elements: [
  disc('verso-folio', 'even', 'body', 'bottom-left', OUTER),
  disc('recto-folio', 'odd', 'body', 'bottom-right', -OUTER),
  disc('opener-folio', 'all', 'opener', 'bottom', 0), // a chapter opens on either side
] };
// #endregion

// #region tailpiece: an unnumbered resource type, set where ::resource stands
// No caption prefix and no caption, so the cherries that close chapter I print with no
// label or number. The pictures the design slots paint are of this type too.
const resourceTypes = [{ id: 'fregio', name: 'Fregio', namePlural: 'Fregi', shortLabel: 'Fregio',
  captionPrefix: '', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal',
  defaultPlacement: { position: 'here', width: 0.16, align: 'center' } }];
// #endregion

// #region pictures: four paintings, declared at their pixel size under the file ids they load as
const PICTURES = { // id: [file, width, height, alt text in the book's language]
  frontespizio: ['frontespizio-1400.jpg', 1400, 1105, 'Colline toscane con cipressi e un borgo; '
    + 'un contadino guida un asinello con un carretto di legna verso la bottega di un falegname.'],
  ceppo: ['ceppo-1200.jpg', 1200, 800, 'Maestro Ciliegia, con la scure alzata sopra un pezzo di '
    + 'legno sul banco, si guarda intorno spaventato.'],
  polenta: ['polenta-1200.jpg', 1200, 800, 'Geppetto, con la parrucca gialla, riceve il pezzo di '
    + 'legno da Maestro Ciliegia; accanto, un paiolo di polenta fumante.'],
  ciliegie: ['ciliegie-600.jpg', 600, 626, 'Due ciliegie rosse su un picciolo, con una foglia.'],
};
const resources = Object.entries(PICTURES).map(([id, [fileId, w, h, altText]]) => ({
  id, typeId: 'fregio', kind: 'bitmap', createdAt: 0, updatedAt: 0, altText,
  bitmap: { fileId, format: 'jpeg', width: w, height: h } }));
// #endregion

const config = () => ({
  locale: 'it', // Italian hyphenation
  resourceTypes, colorPalette, header, footer, layout: { layoutType: 'single' },
  page: { width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'),
    margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
      mirror: true } },
  bodyText: {
    fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    italicColor: col('ink'), // *ohi* in ink, not in the defaults' bark
    referenceColor: col('ink'), // the book cites nothing; a :ref added later prints in ink too
    firstLineIndent: mm(5), indentAfterHeading: false,
    // Word spaces from 0.8 to 1.45 of the face's own, and up to 8 ‰ of tracking for a line
    // that would pass either limit. The default maximum of 2 lets 14 lines stretch past
    // 1.45×; 1.45 alone left three, to 1.58×, beside lines squeezed towards the default 0.6.
    minWordSpacing: 0.8, maxWordSpacing: 1.45, maxJustifyTracking: 8,
  },
  headings: {
    fontFamily: DISPLAY, fontWeight: 400, // the designs paint the titles: no Open Sans to load
    // Every chapter on a new page, on either side, so chapter II opens on a verso.
    levels: [{ level: 1, breakBefore: { enabled: true, parity: 'any' } }],
  },
  headingStyles: [frontispiece, chapter],
  paragraphStyles: [{ id: 'colofon', fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(11),
    color: col('bark'), textAlign: 'center', firstLineIndent: pt(0), marginTop: pt(LEAD) }],
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Le avventure di Pinocchio"
subtitle: "Storia di un burattino"
author: "Carlo Collodi"
---

# Pinocchio {style="frontespizio" kicker="Le avventure di"}

# Capitolo I {style="capitolo" vignetta="ceppo" summary="Come andò che Maestro Ciliegia, falegname, trovò un pezzo di legno che piangeva e rideva come un bambino."}

— C’era una volta…

— Un re! — diranno subito i miei piccoli lettori.

— No, ragazzi, avete sbagliato. C’era una volta un pezzo di legno.

Non era un legno di lusso, ma un semplice pezzo da catasta, di quelli che d’inverno si mettono nelle stufe e nei caminetti per accendere il fuoco e per riscaldare le stanze.

Non so come andasse, ma il fatto gli è che un bel giorno questo pezzo di legno capitò nella bottega di un vecchio falegname, il quale aveva nome mastr’Antonio, se non che tutti lo chiamavano maestro Ciliegia, per via della punta del suo naso, che era sempre lustra e paonazza, come una ciliegia matura.

Appena maestro Ciliegia ebbe visto quel pezzo di legno, si rallegrò tutto; e dandosi una fregatina di mani per la contentezza, borbottò a mezza voce:

— Questo legno è capitato a tempo; voglio servirmene per fare una gamba di tavolino. —

Detto fatto, prese subito l’ascia arrotata per cominciare a levargli la scorza e a digrossarlo; ma quando fu lì per lasciare andare la prima asciata, rimase col braccio sospeso in aria, perchè sentì una vocina sottile sottile, che disse raccomandandosi:

— Non mi picchiar tanto forte! —

Figuratevi come rimase quel buon vecchio di maestro Ciliegia!

Girò gli occhi smarriti intorno alla stanza per vedere di dove mai poteva essere uscita quella vocina, e non vide nessuno! Guardò sotto il banco, e nessuno: guardò dentro un armadio che stava sempre chiuso, e nessuno; guardò nel corbello dei trucioli e della segatura, e nessuno; aprì l’uscio di bottega per dare un’occhiata anche sulla strada, e nessuno. O dunque?…

— Ho capito; — disse allora ridendo e grattandosi la parrucca — si vede che quella vocina me la son figurata io. Rimettiamoci a lavorare. —

E ripresa l’ascia in mano, tirò giù un solennissimo colpo sul pezzo di legno.

— Ohi! tu m’hai fatto male! — gridò rammaricandosi la solita vocina.

Questa volta maestro Ciliegia restò di stucco, cogli occhi fuori del capo per la paura, colla bocca spalancata e colla lingua giù ciondoloni fino al mento, come un mascherone da fontana.

Appena riebbe l’uso della parola, cominciò a dire tremando e balbettando dallo spavento:

— Ma di dove sarà uscita questa vocina che ha detto ohi?… Eppure qui non c’è anima viva. Che sia per caso questo pezzo di legno che abbia imparato a piangere e a lamentarsi come un bambino? Io non lo posso credere. Questo legno eccolo qui; è un pezzo di legno da caminetto, come tutti gli altri, e a buttarlo sul fuoco, c’è da far bollire una pentola di fagioli… O dunque? Che ci sia nascosto dentro qualcuno? Se c’è nascosto qualcuno, tanto peggio per lui. Ora l’accomodo io! —

E così dicendo, agguantò con tutt’e due le mani quel povero pezzo di legno, e si pose a sbatacchiarlo senza carità contro le pareti della stanza.

Poi si messe in ascolto, per sentire se c’era qualche vocina che si lamentasse. Aspettò due minuti, e nulla; cinque minuti, e nulla; dieci minuti, e nulla!

— Ho capito — disse allora sforzandosi di ridere e arruffandosi la parrucca — si vede che quella vocina che ha detto *ohi*, me la son figurata io! Rimettiamoci a lavorare. —

E perchè gli era entrato addosso una gran paura, si provò a canterellare per farsi un po’ di coraggio.

Intanto, posata da una parte l’ascia, prese in mano la pialla, per piallare e tirare a pulimento il pezzo di legno; ma nel mentre che lo piallava in su e in giù, sentì la solita vocina che gli disse ridendo:

— Smetti! tu mi fai il pizzicorino sul corpo! —

Questa volta il povero maestro Ciliegia cadde giù come fulminato. Quando riaprì gli occhi, si trovò seduto per terra.

Il suo viso pareva trasfigurito, e perfino la punta del naso, di paonazza come era quasi sempre, gli era diventata turchina dalla gran paura.

::resource{id="ciliegie"}

# Capitolo II {style="capitolo" vignetta="polenta" summary="Maestro Ciliegia regala il pezzo di legno al suo amico Geppetto, il quale lo prende per fabbricarsi un burattino maraviglioso, che sappia ballare, tirar di scherma e fare i salti mortali."}

In quel punto fu bussato alla porta.

— Passate pure, — disse il falegname, senza aver la forza di rizzarsi in piedi.

Allora entrò in bottega un vecchietto tutto arzillo, il quale aveva nome Geppetto; ma i ragazzi del vicinato, quando lo volevano far montare su tutte le furie, lo chiamavano col soprannome di Polendina, a motivo della sua parrucca gialla, che somigliava moltissimo alla polendina di granturco.

Geppetto era bizzosissimo. Guai a chiamarlo Polendina! Diventava subito una bestia, e non c’era più verso di tenerlo.

:::paragraphs{style="colofon"}
Composto in Averia Serif Libre, Fredericka the Great e Quicksand (SIL Open Font License). Testo di Carlo Collodi (1883) secondo l’edizione Bemporad del 1902, Project Gutenberg n. 52484. Illustrazioni generate con modelli di diffusione.
:::
`; // content.<lang>.md: Collodi's Italian in both editions


// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { 'Averia Serif Libre': ['400', '400i'], 'Fredericka the Great': ['400'],
  Quicksand: ['400', '600', '700'] }; // every face, loaded before layout

// ─── 4 · Build & show ───────────────────────────────────────────────────────
// A design slot or ::resource names a resource, the resource a file id: load each file.
await Promise.all(Object.values(PICTURES).map(([file]) => loadImage(file, asset(file))));
const doc = await buildDocumentWithFonts({ markdown, resources, continuation }, config(),
  kitFonts(FONTS));
showPages(doc, { title: 'Le avventure di Pinocchio' });

// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ─────

// ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook
function mm(value) { return { value, unit: 'mm' }; }
function pt(value) { return { value, unit: 'pt' }; }
function em(value) { return { value, unit: 'em' }; }
/** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */
function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; }
/** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */
function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; }

// ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook
// Postext loads the faces a config asks for before layout (prepareFonts,
// buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the
// files the PDF embeds too.

/** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts:
 *  the Fontsource resolver, and the faces of FONTS loaded with the ones the
 *  config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */
function kitFonts(FONTS = {}) {
  const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) =>
    ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' })));
  return { resolve: fontsourceResolver, faces };
}

/** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek
 *  when `text` needs them and the family ships them (kitSubsetsFor), each with
 *  its unicode-range. Null for a weight or slant Fontsource does not ship. */
async function fontsourceResolver(family, weight, style, { text = '' } = {}) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null;
  kitStatus('Loading fonts…');
  const at = await fontsourceAt(id);
  return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({
    source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`,
    unicodeRange: kitRanges()[subset],
  }));
}

/** The unicode-range of each Fontsource file the kit loads (a function: the
 *  kit is inlined after the pen's code, which runs before a const would be set). */
function kitRanges() {
  return {
    latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,'
      + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
    'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,'
      + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF',
    greek: 'U+0370-03FF',
  };
}

/** For the cjk, arabic and comics blocks, which add their own files: the
 *  faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  The files are added once all have loaded, in the order asked (latin, then
 *  latin-ext, then greek), whatever order they arrive in: the last one added
 *  wins a character two files hold. Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = kitRanges();
  const jobs = [];
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal'])
      .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await
    const at = fontsourceAt(id);
    const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null;
    const subsets = ['latin', ...kitSubsetsFor(text, meta)];
    for (const [weight, style] of todo) {
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`;
        const face = new FontFace(family, `url(${url}) format('woff2')`,
          { weight: String(weight), style, unicodeRange: ranges[subset] });
        jobs.push(face.load().catch(() => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return ready.filter((face) => face && document.fonts.add(face)).length;
}

/** A loaded FontFace covers this family, weight and style (fonts.check() would
 *  also say yes for families nobody declared). */
function hasFace(family, weight, style) {
  for (const face of document.fonts) {
    if (face.status !== 'loaded' || face.style !== style) continue;
    if (face.family.replace(/^["']|["']$/g, '') !== family) continue;
    const [low, high = low] = face.weight.split(' ').map(Number);
    if (weight >= low && weight <= high) return true;
  }
  return false;
}

/** The files beyond latin `text` needs that `meta`'s family ships. */
function kitSubsetsFor(text, meta) {
  return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']]
    .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x);
}

/** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */
function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); }

/** A package's folder at the one version @5 names now (#766). */
function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) {
  return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json())
    .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`);
}

/** The family's Fontsource metadata (weights, styles, subsets), or null. */
function fontsourceMeta(family) {
  fontsourceMeta.cache ??= new Map();
  const id = fontsourceId(family);
  if (!fontsourceMeta.cache.has(id)) {
    fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`)
      .then((res) => (res.ok ? res.json() : null), () => null));
  }
  return fontsourceMeta.cache.get(id);
}

// ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook
/** The pages as spreads on a dark desk, page 1 alone, then verso | recto,
 *  each painted when it scrolls near. Other options go to renderPageToCanvas. */
function showPages(docs, { title, width = 460, ...render } = {}) {
  const root = viewer(title);
  const pages = [docs].flat().flatMap((doc) =>
    doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index })));
  const spreads = [];
  let verso = null;
  for (const p of pages) {
    if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; }
    else { spreads.push([verso, p]); verso = null; }
  }
  if (verso) spreads.push([verso, null]);
  const density = Math.min(window.devicePixelRatio || 1, 2);
  showPages.painter?.disconnect();
  const painter = new IntersectionObserver((entries) => {
    for (const { isIntersecting, target } of entries) {
      if (!isIntersecting) continue;
      painter.unobserve(target);
      const { doc, page } = target.postext;
      renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width });
    }
  }, { rootMargin: '800px' });
  showPages.painter = painter;
  root.replaceChildren(...spreads.map((pair) => {
    const spread = document.createElement('div');
    spread.className = 'pt-spread';
    for (const p of pair) {
      const figure = document.createElement('figure');
      if (p) {
        const label = p.page.pageLabel || String(p.n + 1);
        const canvas = document.createElement('canvas');
        canvas.postext = p;
        canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`;
        canvas.setAttribute('role', 'img');
        canvas.setAttribute('aria-label', `Page ${label}`);
        const folio = document.createElement('figcaption');
        folio.textContent = label;
        figure.append(canvas, folio);
        painter.observe(canvas);
      } else figure.className = 'pt-blank';
      spread.append(figure);
    }
    return spread;
  }));
  kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`);
  document.documentElement.dataset.postext = 'ready';
  return pages.length;
}

/** The desk, the bar and the error reporting, created once. */
function viewer(title) {
  if (!document.getElementById('pt-kit')) {
    document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit">
      :root { color-scheme: dark; }
      body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; }
      #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center;
        gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px);
        border-bottom: 1px solid #23262d; }
      #pt-bar strong { color: #f4f1ea; font-weight: 600; }
      #pt-actions { display: flex; gap: 12px; margin-left: auto; }
      #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
      #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; }
      .pt-spread { display: flex; }
      .pt-spread figure { margin: 0; width: min(460px, 44vw); }
      .pt-spread canvas { display: block; width: 100%; background: #fff;
        box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif;
        letter-spacing: .18em; text-transform: uppercase; color: #6c7079; }
      .pt-blank { visibility: hidden; }
      @media (max-width: 760px) {
        .pt-spread { flex-direction: column; gap: 32px; }
        .pt-spread figure { width: min(460px, 92vw); }
        .pt-blank { display: none; }
      }
    </style>`);
    document.body.insertAdjacentHTML('afterbegin',
      '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>');
    document.getElementById('pt-title').textContent = document.title || 'Postext';
    addEventListener('error', (event) => kitFail(event.error ?? event.message));
    addEventListener('unhandledrejection', (event) => kitFail(event.reason));
  }
  if (title) document.getElementById('pt-title').textContent = title;
  return document.getElementById('pages')
    ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' }));
}

function kitStatus(text) {
  viewer();
  document.getElementById('pt-status').textContent = text;
}

function kitFail(error) {
  document.documentElement.dataset.postext = 'error';
  kitStatus(`Error: ${error?.message ?? error}`);
}

// ─── Kit · images v3 ── recipes with pictures · postext.dev/cookbook
/** Registers a photo or PNG for the canvas and keeps its bytes for the PDF.
 *  fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */
async function loadImage(fileId, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`);
  const bytes = new Uint8Array(await res.arrayBuffer());
  registerResourceImage(fileId, await createImageBitmap(new Blob([bytes])));
  (loadImage.bytes ??= new Map()).set(fileId, bytes);
}

/** Registers SVG markup as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25).
 *  `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */
async function loadSvg(fileId, svg, options) {
  if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces);
  const img = new Image();
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await img.decode();
  registerResourceImage(fileId, img, options);
  (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg));
}

/** A face's Fontsource files: nearest weight; latin + what the SVG sets. */
async function svgFaces(family, weight, style, { codePoints = [] } = {}) {
  const [id, m] = [fontsourceId(family), await fontsourceMeta(family)];
  const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight;
  const s = m?.styles.includes(style) === false ? 'normal' : style;
  return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array(
    await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer())));
}

/** renderToPdf({ resourceBytes: imageBytes }) */
function imageBytes(fileId) { return loadImage.bytes?.get(fileId); }

/** renderToHtml({ resourceImageUrl: imageUrl }) */
function imageUrl(fileId) {
  const bytes = imageBytes(fileId);
  if (!bytes) return undefined;
  imageUrl.urls ??= new Map();
  if (!imageUrl.urls.has(fileId)) {
    const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg';
    imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type })));
  }
  return imageUrl.urls.get(fileId);
}

// ─── /Kit ───────────────────────────────────────────────────────────────────────
```

## Varianti

### Apri ogni capitolo su un recto

Con `'odd'` il capitolo II passa a pagina 11 e pagina 10 resta bianca; il frontespizio disattiva la propria interruzione, perché eredita quella del livello e altrimenti passerebbe a pagina 7 dopo una pagina 6 bianca.

```diff
@@ the frontispiece style @@
   id: 'frontespizio',
+  breakBefore: { enabled: false },
@@ headings, in config() @@
-    levels: [{ level: 1, breakBefore: { enabled: true, parity: 'any' } }],
+    levels: [{ level: 1, breakBefore: { enabled: true, parity: 'odd' } }],
```

## Errori frequenti

- **Valori degli attributi: niente { o }; virgolette semplici per un valore con ".** Il valore di un attributo finisce alla graffa di chiusura, quindi non può contenere { o }. Un valore che contiene una virgoletta doppia va tra virgolette semplici; il simbolo del dollaro va bene.

- Nel Markdown uno spazio non divisibile (U+00A0) lega ogni lineetta di dialogo di Collodi alla sua parola, così una lineetta di chiusura non apre mai una riga; altri due tengono uniti *di legno* e *in piedi* alla fine dei loro paragrafi. I legami tengono anche in un paragrafo con grassetto o corsivo, come quello con *ohi* a pagina 9, che ne ha quattro.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Testo: Le avventure di Pinocchio (1883): chapter I and the opening of chapter II with Collodi’s chapter summaries, from the 1902 Bemporad edition, with typographic apostrophes, dashes and ellipses; chapter I’s summary has the comma after “falegname” that the edition’s table of contents prints and its chapter head leaves out: Carlo Collodi ([fonte](https://www.gutenberg.org/ebooks/52484)), pubblico dominio
- Immagini: The frontispiece: Tuscan hills, a hill village and a donkey cart of firewood on its way to the carpenter’s workshop: Generated With Diffusion Models, originale
- Immagini: Chapter I’s vignette: Maestro Ciliegia, axe raised over the talking log: Generated With Diffusion Models, originale
- Immagini: Chapter II’s vignette: Geppetto in his yellow wig receives the log, beside a pot of polenta: Generated With Diffusion Models, originale
- Immagini: The cherry tailpiece: Generated With Diffusion Models, originale
- Caratteri: Averia Serif Libre (OFL-1.1), Fredericka the Great (OFL-1.1), Quicksand (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 017 · Cinque aperture di capitolo in un libro](https://postext.dev/it/cookbook/five-chapter-openers.md): Un’apertura sul livello 1 e quattro stili di titolo nominati nel Markdown; ogni stile cambia il colore d’accento, e alcuni anche margini, colonne o folio. · Livello 3 (Avanzato) · Qualsiasi genere
- [Nº 016 · Spagnolo giustificato in un romanzo tascabile](https://postext.dev/it/cookbook/spanish-pocket-novel.md): L’inizio del capitolo I di Marianela in tascabile: sillabazione spagnola, spazi sotto 1,7×, nessuna riga finale corta, iniziale rialzata, Figura 1 sulla mappa. · Livello 2 (Intermedio) · Narrativa, teatro e prosa letteraria
- [Nº 003 · Apertura di capitolo su una fascia al vivo](https://postext.dev/it/cookbook/chapter-opener-bleed-band.md): Un’apertura advancedDesign sul titolo di livello 1: una fascia al vivo, il numero del capitolo sul suo piede, occhiello e sommario dagli attributi del titolo. · Livello 3 (Avanzato) · Libri di testo
