# Romanzo in brossura: aperture abbassate e capitoli sul recto

> Pinocchio in brossura da 140 × 216 mm: una copertina dipinta e un colophon senza testatine, poi capitoli abbassati sui recto sotto un numero in corsivo.

- Versione HTML: https://postext.dev/it/cookbook/trade-paperback-novel
- Ricetta Nº 038 · Pubblicazioni complete · Livello 2 (Intermedio) · Output: Canvas
- Generi: Narrativa, teatro e prosa letteraria
- Richiede postext ≥ 1.25.0 · provata con 1.33.7 il 2026-10-15
- Pagine: [1](https://postext.dev/cookbook/trade-paperback-novel/it/p01.webp?v=72a2e1d8), [2](https://postext.dev/cookbook/trade-paperback-novel/it/p02.webp?v=72a2e1d8), [3](https://postext.dev/cookbook/trade-paperback-novel/it/p03.webp?v=72a2e1d8), [4](https://postext.dev/cookbook/trade-paperback-novel/it/p04.webp?v=72a2e1d8), [5](https://postext.dev/cookbook/trade-paperback-novel/it/p05.webp?v=72a2e1d8), [6](https://postext.dev/cookbook/trade-paperback-novel/it/p06.webp?v=72a2e1d8), [7](https://postext.dev/cookbook/trade-paperback-novel/it/p07.webp?v=72a2e1d8), [8](https://postext.dev/cookbook/trade-paperback-novel/it/p08.webp?v=72a2e1d8), [9](https://postext.dev/cookbook/trade-paperback-novel/it/p09.webp?v=72a2e1d8)
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=trade-paperback-novel&lang=it (.postext: https://postext.dev/cookbook/trade-paperback-novel/it/trade-paperback-novel.postext)
- Ultimo aggiornamento: 2026-10-10
- Altre lingue: [en](https://postext.dev/en/cookbook/trade-paperback-novel.md), [es](https://postext.dev/es/cookbook/trade-paperback-novel.md), [ca](https://postext.dev/ca/cookbook/trade-paperback-novel.md), [de](https://postext.dev/de/cookbook/trade-paperback-novel.md), [fr](https://postext.dev/fr/cookbook/trade-paperback-novel.md), [pt](https://postext.dev/pt/cookbook/trade-paperback-novel.md), [zh](https://postext.dev/zh/cookbook/trade-paperback-novel.md), [ja](https://postext.dev/ja/cookbook/trade-paperback-novel.md), [ar](https://postext.dev/ar/cookbook/trade-paperback-novel.md)

## In breve

I primi due capitoli di un romanzo italiano in un libro in brossura. Ogni capitolo comincia su una pagina di destra, più in basso, sotto un grande numero di capitolo.

## Che cosa comporrai

Nove pagine di un romanzo in brossura da 140 × 216 mm contengono i primi due capitoli di *Le avventure di Pinocchio* (1883) di Carlo Collodi. La copertina, caricata come JPEG, è un particolare di *Settignano* (1880 circa), un dipinto di Telemaco Signorini: una strada con la bottega del pane e del vino, una donna seduta sulla soglia e un gatto sul selciato al sole. Il colophon sta al piede del suo verso. Ogni capitolo si apre su un recto con il testo a 81 mm dalla testa, sotto un numero in corsivo da 48 pt, un filetto sottile color ruggine di 18 mm e la prima frase del capitolo in maiuscoletto, centrata: in tutti e due i capitoli sta su una riga. Il capitolo I finisce a pagina 5, dopo quattro righe, con un finalino a fiore di camomilla, quindi la pagina 6 resta bianca, senza testatina né folio, e il capitolo II si apre a pagina 7 e finisce a pagina 9. Le testatine portano l’autore sui versi e il titolo in corsivo sui recto.

**Questa ricetta risponde a:**

- Come faccio cominciare ogni capitolo su una pagina di destra, abbassato di un terzo, come in una brossura?
- Come aggiungo a un’apertura una riga d’autore, un sommario o un attacco con capolettera?
- Come inserisco pagine bianche volute, o faccio cominciare una sezione su una nuova doppia pagina?
- Come imposto le testatine: titolo del libro sulla pagina sinistra, titolo del capitolo sulla destra, numero di pagina all’esterno?
- Come creo una copertina, un occhietto, un frontespizio e un colophon?
- Come aggiungo spazio verticale tra due blocchi, se le righe vuote non hanno effetto?
- Come compongo illustrazioni non numerate: fregi, vignette, loghi?

## La risposta breve

```js
// script.js, righe 47–77
// # I {lead="A green and yellow parrot, … kept repeating over and over:"}
// The heading's text is the numeral, printed by {titleText}; the chapter's first sentence
// travels as the heading's lead attribute, printed by {attr.lead} in small capitals: a
// centred display, which a \n in the attribute breaks by sense.
const [LEAD_AT, BODY_AT] = [9, 12]; // grid lines: the lead's top; where the text starts
const NUMERAL = 48; // pt
const RULE_Y = (LEAD_AT - 1.5) * LEAD * MM_PER_PT; // mm: a line and a half above the lead
const NUMERAL_Y = RULE_Y - NUMERAL * MM_PER_PT - 3; // mm: the numeral's box ends 3 mm above it
const centred = (y) => ({ anchor: { to: 'container', edge: 'top' }, offset: { y } });
const opener = {
  enabled: true,
  // The two-line lead ends on line LEAD_AT + 2; minHeight leaves a blank line under it and
  // starts the text on line BODY_AT (a one-line lead would leave two).
  minHeight: line(BODY_AT),
  slot: { elements: [
    { kind: 'text', id: 'numeral', content: '{titleText}', fontFamily: DISPLAY, italic: true,
      fontWeight: 500, fontSize: pt(NUMERAL), lineHeight: 1, color: col('ink'),
      align: 'center', placement: centred(mm(NUMERAL_Y)) },
    { kind: 'rule', id: 'hairline', direction: 'horizontal', thickness: pt(0.6),
      color: col('rubric'), placement: { ...centred(mm(RULE_Y)), size: { width: mm(18) } } },
    { kind: 'text', id: 'lead', content: '{attr.lead}', fontFamily: LABEL, fontWeight: 600,
      fontSize: pt(9.5), letterSpacing: pt(0.6), color: col('ink'), align: 'center',
      lineHeight: LEAD / 9.5, // one grid line to each line of the lead
      placement: { ...centred(line(LEAD_AT)), size: { width: mm(MEASURE) } } },
  ] },
};
// Every chapter opens on a recto: 'odd' adds a blank verso only after a chapter that ends on
// a recto, where the default 'always-odd' leaves a blank page after every chapter.
// marginBottom 0: the level's default 0.5 em would start the text a line lower.
const chapter = { level: 1, breakBefore: { enabled: true, parity: 'odd' },
  marginBottom: pt(0), italic: true, 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.
- [Capitoli che si aprono su un recto](https://postext.dev/it/docs/configuration-text.md#interruzione-prima-del-titolo): Un livello di titolo comincia una nuova pagina: la successiva, il recto successivo o il verso successivo, aggiungendo una pagina bianca se serve.
- [Testatine per sezione](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli): Uno stile di titolo porta la sua intestazione e il suo piè di pagina, così pagine preliminari, sezioni o lettere di un dizionario hanno il loro corredo di pagina.

**Usa anche**

- [Margini speculari](https://postext.dev/it/docs/configuration-page-layout.md#margini-speculari)
- [Stili di titolo](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)
- [Copertine, frontespizi e colophon](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)
- [Testo, filetti e riquadri nei progetti di pagina](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Immagini nei progetti di pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-immagine)
- [Geometria della sezione](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)
- [Testatine secondo il ruolo della pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)
- [Testatine e folio](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Metadati del documento](https://postext.dev/it/docs/document-format.md#frontmatter)
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Spazio verticale esplicito](https://postext.dev/it/docs/document-format.md#space)
- [Figure e tabelle come risorse](https://postext.dev/it/docs/document-format.md#risorse)
- [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)
- [Colore della carta](https://postext.dev/it/docs/configuration-page-layout.md#pagina)
- [Vedove, orfane e righe corte](https://postext.dev/it/docs/configuration-text.md#orfane-vedove-righe-finali-corte-e-regole-per-tenere-unito)

**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**

- Crimson Pro (OFL-1.1), Cormorant Garamond (OFL-1.1), Cormorant SC (OFL-1.1)

## Preparazione

### 1 · L’apertura viene dal titolo e dal suo attributo

Il codice è nella [risposta breve](https://postext.dev/it/cookbook/trade-paperback-novel.md#la-risposta-breve) qui sopra. Nella prima edizione ogni capitolo di Collodi si apre con un sommario, che questa edizione lascia fuori: ogni titolo contiene solo il suo numero (`# I`), che `{titleText}` stampa. `{attr.lead}` stampa l’attributo `lead` del titolo, che contiene la prima frase del capitolo; il paragrafo Markdown sotto il titolo comincia con la frase successiva ([attributi del titolo](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)). L’attacco è un testo centrato da titolazione. Quello del capitolo I, la battuta *— C’era una volta....*, sta su una riga, e così quello del capitolo II, *In quel punto fu bussato alla porta.*: nessuno dei due attributi ha un `\n`, che nell’edizione inglese spezza la prima frase secondo il senso, dopo *cage* e dopo *bright;*. Il riquadro dell’attacco comincia nove righe della griglia sotto la testa della gabbia, e un `minHeight` di 12 righe lascia due righe vuote sotto un attacco di una riga e una sotto un attacco di due righe: il testo di ogni capitolo comincia sulla stessa riga della griglia. Senza `minHeight` il testo comincia subito sotto l’attacco ([span e design avanzato](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato)). `marginBottom: pt(0)` sostituisce il margine predefinito di mezzo em del livello, che spingerebbe il testo una riga più in basso.

`breakBefore` con parità `'odd'` manda ogni capitolo su un recto, con un verso bianco solo dopo un capitolo che finisce su un recto ([interruzione prima](https://postext.dev/it/docs/configuration-text.md#interruzione-prima-del-titolo)). Per forzare un recto dove non c’è un titolo, scrivi `:::pagebreak{parity="odd"}` nel Markdown; in un’impaginazione di due o più colonne, `:::columnbreak` sposta invece il testo alla colonna successiva ([`:::pagebreak`](https://postext.dev/it/docs/document-format.md#pagebreak)).

### 2 · Una pagina in brossura di righe intere

```js
// script.js, righe 33–43
const TRIM = { width: 140, height: 216 }; // mm: 5½ × 8½ in
const [TOP, INNER, OUTER] = [22, 20, 15.5]; // mm; mirrored, so INNER is left on a recto
const LINES = 35; // every full page ends on the same line
const MEASURE = TRIM.width - INNER - OUTER; // 104.5 mm: about 69 characters of Crimson Pro
const MM_PER_PT = 25.4 / 72;
const page = {
  sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150,
  backgroundColor: col('paper'),
  margins: { top: mm(TOP), bottom: mm(TRIM.height - TOP - LINES * LEAD * MM_PER_PT),
    left: mm(INNER), right: mm(OUTER), mirror: true },
};
```

Il margine inferiore è ciò che il formato di 216 mm lascia dopo il margine superiore e 35 righe da 14 pt, così ogni pagina piena finisce sulla stessa riga, come fanno le pagine 3, 4 e 8. La pagina 7 finisce una riga più in alto, per non lasciare da sola in testa alla pagina 8 l’ultima riga di un paragrafo. Con `mirror: true` il margine interno di 20 mm cade sul lato del dorso sia nei recto sia nei versi ([margini speculari](https://postext.dev/it/docs/configuration-page-layout.md#margini-speculari)). La giustezza di 104,5 mm contiene circa 69 caratteri di Crimson Pro a 10,5 pt. Il testo mantiene gli spazi tra le parole tra 0,7 e 1,9 volte la larghezza normale; con il minimo predefinito di 0,6, una riga dell’edizione inglese si stringe a 0,62. Nell’edizione italiana nessuna riga supera 1,9 (1,87 al massimo).

### 3 · Testatine che saltano aperture e pagine bianche

```js
// script.js, righe 81–101
// {author} and {title} come from the frontmatter.
const HEAD_Y = 12; // mm from the top edge to the top of the running heads
const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre is off the page's centre
const head = (id, content, parity, edge, x, style) => ({
  kind: 'text', id, content, parity, pages: 'body', // never on openers or blank pages
  fontSize: pt(9), color: col('muted'), ...style,
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD_Y) } },
});
const smallCaps = { fontFamily: LABEL, fontWeight: 600, letterSpacing: pt(1.2) };
const italic = { fontFamily: DISPLAY, italic: true, fontWeight: 500, fontSize: pt(9.75) };
const folio = { fontFamily: TEXT, color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('verso-author', '{author}', 'even', 'top', -SHIFT, smallCaps),
  head('recto-title', '{title}', 'odd', 'top', SHIFT, italic),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };
// Openers carry a drop folio instead, centred under the text block.
const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
  pages: 'opener', fontFamily: TEXT, fontSize: pt(9), color: col('muted'), align: 'center',
  placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(7) } } }] };
```

`parity: 'even'` mette l’autore in maiuscoletto Cormorant SC sui versi, e `'odd'` il titolo in corsivo sui recto; i folii stanno sul bordo esterno della gabbia. `pages: 'body'` tiene tutti e quattro gli elementi lontani dalle aperture e dalla pagina 6, il verso bianco che aggiunge l’interruzione per parità. Con `pages: 'all'` quella pagina stamperebbe *6* e *Carlo Collodi*. Le aperture ricevono invece un folio al piede, un elemento del piè di pagina che `pages: 'opener'` limita a esse ([elementi di testo](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)).

### 4 · La copertina è una sezione senza testatine

```js
// script.js, righe 105–128
// Page 1 is a recto (gotcha: parity-page1-recto): the cover, the colophon on its verso, and
// chapter I on page 3. # The Awakening {style="cover"} opens the section; chapter I closes it.
// The colophon: 2 + 2 + 1 lines of 11 pt and two gaps of 14.5 pt make 84 pt, six grid lines.
const COLOPHON_LINES = 6;
const COLOPHON_TOP = TOP + (LINES - COLOPHON_LINES) * LEAD * MM_PER_PT; // mm: the section's margin
const onPage = (y) => ({ anchor: { to: 'page', edge: 'top' }, offset: { y: mm(y) } }); // y mm down
const cover = {
  id: 'cover',
  header: { elements: [] }, footer: { elements: [] }, // no running heads on p. 1 or p. 2
  margins: { top: mm(COLOPHON_TOP) },
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'image', id: 'art', resourceId: 'cover',
      placement: { anchor: { to: 'bleed', edge: 'top-left' }, size: { width: 'fill' } } },
    { kind: 'text', id: 'title', content: '{title}', fontFamily: DISPLAY, italic: true,
      fontWeight: 500, fontSize: pt(50), lineHeight: 1, color: col('ink'), align: 'center',
      placement: onPage(155) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.6),
      color: col('rubric'), placement: { ...onPage(179), size: { width: mm(18) } } },
    { kind: 'text', id: 'author', content: '{author}', ...smallCaps, fontSize: pt(12),
      letterSpacing: pt(2.4), color: col('rubric'), align: 'center', placement: onPage(184) },
  ] } },
};
const colophon = { id: 'colophon', fontSize: pt(8), lineHeight: pt(11), color: col('muted'),
  textAlign: 'left', firstLineIndent: pt(0), spaceBetween: pt(14.5) };
```

La copertina è un titolo, `# Pinocchio {style="cover"}`. Il suo stile colloca il dipinto sui 134 mm superiori, il titolo del frontmatter a 50 pt e l’autore in maiuscoletto. Lo stile si applica anche alla sezione che il titolo apre, le pagine 1 e 2, fino al capitolo I. I suoi `header` e `footer` vuoti tengono il folio al piede lontano dalla copertina e le testatine lontane dal colophon. Il suo margine superiore mette il colophon sulle ultime sei righe della griglia della gabbia, così l’ultima riga del colophon è allineata con l’ultima riga di pagina 3 ([stili dei titoli](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)). La colonna della sezione comincia a 165 mm dalla testa, e il design, ancorato alla pagina e all’abbondanza, dipinge comunque sopra di essa: solo il piede di una colonna taglia un design tenuto al suo interno.

![Pagina 2. Il colophon, composto in piccolo al piede della pagina, senza testatina né folio: «Le avventure di Pinocchio uscì in volume da Felice Paggi, a Firenze, nel 1883.»](https://postext.dev/cookbook/trade-paperback-novel/it/p02.webp?v=72a2e1d8)

*Pagina 2: il colophon al piede del verso della copertina, senza testatina né folio.*

Il dipinto cambia con l’edizione: `t()` sceglie il file della risorsa `cover`, e l’edizione italiana carica `cover-it-1190.jpg`, un ritaglio di *Settignano* alle stesse proporzioni di 140 × 134 mm e agli stessi pixel di `cover-1190.jpg`, che le altre edizioni caricano. Il frontmatter dà il titolo breve, *Pinocchio*, che sta su una riga a 50 pt; il titolo completo del libro è nel colophon.

### 5 · Un finalino chiude ogni capitolo

```js
// script.js, righe 132–164
// At a chapter's end: :::space, then ::resource{id="tailpiece-1"} (double quotes: gotcha
// resource-double-quotes). The space adds a line to the gap the embed keeps above it.
const ornament = { id: 'ornament', name: 'Ornament', shortLabel: '', captionPrefix: '',
  numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal',
  defaultPlacement: { position: 'here', width: 0.17, align: 'center' } };
const svg = (id, fileId, width, height, altText) => ({ id, typeId: 'ornament', kind: 'svg',
  svg: { fileId, width, height }, altText, createdAt: 0, updatedAt: 0 });
// The cover is a painting, a JPEG in assets/ cut to the art's 140 × 134 mm, declared at its pixels;
// the it edition's is a detail of Signorini's Settignano.
const painting = { id: 'cover', typeId: 'ornament', kind: 'bitmap', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: t({ en: 'cover-1190.jpg', it: 'cover-it-1190.jpg' }), format: 'jpeg',
    width: 1190, height: 1139 },
  altText: t({ en: 'The gulf seen between the trunks of water-oaks, a lugger on the horizon; a '
    + 'white sunshade comes up from the beach through the camomile.',
  pt: 'O mar visto entre os troncos de duas árvores, um veleiro no horizonte; uma sombrinha '
    + 'branca sobe da praia por um caminho entre flores amarelas e brancas.',
  fr: 'La mer vue entre les troncs de deux arbres, un voilier à l’horizon ; une ombrelle '
    + 'blanche monte de la plage par un chemin entre des fleurs jaunes et blanches.',
  de: 'Das Meer zwischen den Stämmen zweier Bäume, ein Segelboot am Horizont; ein weißer '
    + 'Sonnenschirm kommt vom Strand einen Weg zwischen gelben und weißen Blumen herauf.',
  it: 'Una strada di Settignano: la bottega del pane e del vino, una donna seduta sulla '
    + 'soglia, un gatto sul selciato al sole.' }) };
const resources = [
  painting,
  // ::resource places each ornament once, so each chapter end has its own id.
  ...t({ en: ['I', 'II'], pt: ['I', 'II', 'III'], fr: ['I', 'II'], de: ['I', 'II'], it: ['I', 'II'] })
    .map((n, i) => svg(`tailpiece-${i + 1}`, 'fleuron.svg', 40, 12,
      t({ en: `A camomile flower between two sprigs closes chapter ${n}.`,
        pt: `Uma flor de camomila entre dois ramos fecha o capítulo ${n}.`,
        fr: `Une fleur de camomille entre deux brins clôt le chapitre ${n}.`,
        de: `Eine Kamillenblüte zwischen zwei Zweigen schließt das Kapitel ${n}.`,
        it: `Un fiore di camomilla tra due rametti chiude il capitolo ${n}.` }))),
];
```

Il tipo ornamento non ha prefisso di didascalia, quindi il fiore di camomilla si stampa senza etichetta, largo il 17% della giustezza, dove sta `::resource{id="tailpiece-1"}` ([tipi di risorsa](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)). Un inserimento in linea mantiene una riga della griglia sopra di sé, e il `:::space` che lo precede ne aggiunge una seconda tra l’ultima riga del capitolo e il fiore ([`:::space`](https://postext.dev/it/docs/document-format.md#space)). Entrambi i finalini disegnano lo stesso file, `fleuron.svg`, con due id, perché una risorsa si colloca una sola volta.

## 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/trade-paperback-novel

### script.js

```js
// ═══ Postext Cookbook · Nº 038 · Trade paperback: sunk openers and recto chapters ═══
// https://postext.dev/en/cookbook/trade-paperback-novel
// Code: MIT · Text: Chopin, Machado, Goncourt, Collodi, Keyserling (PD) · Covers: diffusion models, Signorini
// Fonts: Crimson Pro, Cormorant Garamond, Cormorant SC (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';

const LANG = 'it'; // @lang: 'en' Chopin|'pt' Machado|'fr' Goncourt|'de' Keyserling|'it' Collodi
const RECIPE = 'trade-paperback-novel';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { // every colour in the config links to one of these
  ink: '#231d18', // the text: a warm near-black
  rubric: '#8e3b22', // the one accent: the opener's hairline, the author, the tailpiece
  muted: '#6e655b', // running heads, folios on openers, the colophon
  paper: '#fbf7ef', // a cream book paper
};
// Each colour names its palette entry and carries its hex.
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 engine's defaults (headings, lists, callouts, captions) link to 'main-color', the rubric.
  { id: 'main-color', name: 'rubric (defaults)', value: { hex: palette.rubric, model: 'hex' } },
];
const TEXT = 'Crimson Pro'; // the text face
const DISPLAY = 'Cormorant Garamond'; // italic numerals and the cover's title
const LABEL = 'Cormorant SC'; // small capitals: the lead, the author's name
const [BODY, LEAD] = [10.5, 14]; // pt: the body size and its leading, the grid's pitch
const line = (n) => pt(n * LEAD); // n grid lines

// #region page: a 140 × 216 mm trade page whose text block holds 35 whole lines
const TRIM = { width: 140, height: 216 }; // mm: 5½ × 8½ in
const [TOP, INNER, OUTER] = [22, 20, 15.5]; // mm; mirrored, so INNER is left on a recto
const LINES = 35; // every full page ends on the same line
const MEASURE = TRIM.width - INNER - OUTER; // 104.5 mm: about 69 characters of Crimson Pro
const MM_PER_PT = 25.4 / 72;
const page = {
  sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150,
  backgroundColor: col('paper'),
  margins: { top: mm(TOP), bottom: mm(TRIM.height - TOP - LINES * LEAD * MM_PER_PT),
    left: mm(INNER), right: mm(OUTER), mirror: true },
};
// #endregion

// #region answer: a sunk opener: the numeral, a hairline, and the first sentence in small caps
// # I {lead="A green and yellow parrot, … kept repeating over and over:"}
// The heading's text is the numeral, printed by {titleText}; the chapter's first sentence
// travels as the heading's lead attribute, printed by {attr.lead} in small capitals: a
// centred display, which a \n in the attribute breaks by sense.
const [LEAD_AT, BODY_AT] = [9, 12]; // grid lines: the lead's top; where the text starts
const NUMERAL = 48; // pt
const RULE_Y = (LEAD_AT - 1.5) * LEAD * MM_PER_PT; // mm: a line and a half above the lead
const NUMERAL_Y = RULE_Y - NUMERAL * MM_PER_PT - 3; // mm: the numeral's box ends 3 mm above it
const centred = (y) => ({ anchor: { to: 'container', edge: 'top' }, offset: { y } });
const opener = {
  enabled: true,
  // The two-line lead ends on line LEAD_AT + 2; minHeight leaves a blank line under it and
  // starts the text on line BODY_AT (a one-line lead would leave two).
  minHeight: line(BODY_AT),
  slot: { elements: [
    { kind: 'text', id: 'numeral', content: '{titleText}', fontFamily: DISPLAY, italic: true,
      fontWeight: 500, fontSize: pt(NUMERAL), lineHeight: 1, color: col('ink'),
      align: 'center', placement: centred(mm(NUMERAL_Y)) },
    { kind: 'rule', id: 'hairline', direction: 'horizontal', thickness: pt(0.6),
      color: col('rubric'), placement: { ...centred(mm(RULE_Y)), size: { width: mm(18) } } },
    { kind: 'text', id: 'lead', content: '{attr.lead}', fontFamily: LABEL, fontWeight: 600,
      fontSize: pt(9.5), letterSpacing: pt(0.6), color: col('ink'), align: 'center',
      lineHeight: LEAD / 9.5, // one grid line to each line of the lead
      placement: { ...centred(line(LEAD_AT)), size: { width: mm(MEASURE) } } },
  ] },
};
// Every chapter opens on a recto: 'odd' adds a blank verso only after a chapter that ends on
// a recto, where the default 'always-odd' leaves a blank page after every chapter.
// marginBottom 0: the level's default 0.5 em would start the text a line lower.
const chapter = { level: 1, breakBefore: { enabled: true, parity: 'odd' },
  marginBottom: pt(0), italic: true, advancedDesign: opener };
// #endregion

// #region heads: the author on the verso, the title on the recto, folios outside
// {author} and {title} come from the frontmatter.
const HEAD_Y = 12; // mm from the top edge to the top of the running heads
const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre is off the page's centre
const head = (id, content, parity, edge, x, style) => ({
  kind: 'text', id, content, parity, pages: 'body', // never on openers or blank pages
  fontSize: pt(9), color: col('muted'), ...style,
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD_Y) } },
});
const smallCaps = { fontFamily: LABEL, fontWeight: 600, letterSpacing: pt(1.2) };
const italic = { fontFamily: DISPLAY, italic: true, fontWeight: 500, fontSize: pt(9.75) };
const folio = { fontFamily: TEXT, color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('verso-author', '{author}', 'even', 'top', -SHIFT, smallCaps),
  head('recto-title', '{title}', 'odd', 'top', SHIFT, italic),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };
// Openers carry a drop folio instead, centred under the text block.
const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
  pages: 'opener', fontFamily: TEXT, fontSize: pt(9), color: col('muted'), align: 'center',
  placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(7) } } }] };
// #endregion

// #region cover: a section of its own: art, title, author, no heads, a colophon on its verso
// Page 1 is a recto (gotcha: parity-page1-recto): the cover, the colophon on its verso, and
// chapter I on page 3. # The Awakening {style="cover"} opens the section; chapter I closes it.
// The colophon: 2 + 2 + 1 lines of 11 pt and two gaps of 14.5 pt make 84 pt, six grid lines.
const COLOPHON_LINES = 6;
const COLOPHON_TOP = TOP + (LINES - COLOPHON_LINES) * LEAD * MM_PER_PT; // mm: the section's margin
const onPage = (y) => ({ anchor: { to: 'page', edge: 'top' }, offset: { y: mm(y) } }); // y mm down
const cover = {
  id: 'cover',
  header: { elements: [] }, footer: { elements: [] }, // no running heads on p. 1 or p. 2
  margins: { top: mm(COLOPHON_TOP) },
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'image', id: 'art', resourceId: 'cover',
      placement: { anchor: { to: 'bleed', edge: 'top-left' }, size: { width: 'fill' } } },
    { kind: 'text', id: 'title', content: '{title}', fontFamily: DISPLAY, italic: true,
      fontWeight: 500, fontSize: pt(50), lineHeight: 1, color: col('ink'), align: 'center',
      placement: onPage(155) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.6),
      color: col('rubric'), placement: { ...onPage(179), size: { width: mm(18) } } },
    { kind: 'text', id: 'author', content: '{author}', ...smallCaps, fontSize: pt(12),
      letterSpacing: pt(2.4), color: col('rubric'), align: 'center', placement: onPage(184) },
  ] } },
};
const colophon = { id: 'colophon', fontSize: pt(8), lineHeight: pt(11), color: col('muted'),
  textAlign: 'left', firstLineIndent: pt(0), spaceBetween: pt(14.5) };
// #endregion

// #region tailpiece: an ornament type that prints no caption, where ::resource stands
// At a chapter's end: :::space, then ::resource{id="tailpiece-1"} (double quotes: gotcha
// resource-double-quotes). The space adds a line to the gap the embed keeps above it.
const ornament = { id: 'ornament', name: 'Ornament', shortLabel: '', captionPrefix: '',
  numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal',
  defaultPlacement: { position: 'here', width: 0.17, align: 'center' } };
const svg = (id, fileId, width, height, altText) => ({ id, typeId: 'ornament', kind: 'svg',
  svg: { fileId, width, height }, altText, createdAt: 0, updatedAt: 0 });
// The cover is a painting, a JPEG in assets/ cut to the art's 140 × 134 mm, declared at its pixels;
// the it edition's is a detail of Signorini's Settignano.
const painting = { id: 'cover', typeId: 'ornament', kind: 'bitmap', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: t({ en: 'cover-1190.jpg', it: 'cover-it-1190.jpg' }), format: 'jpeg',
    width: 1190, height: 1139 },
  altText: t({ en: 'The gulf seen between the trunks of water-oaks, a lugger on the horizon; a '
    + 'white sunshade comes up from the beach through the camomile.',
  pt: 'O mar visto entre os troncos de duas árvores, um veleiro no horizonte; uma sombrinha '
    + 'branca sobe da praia por um caminho entre flores amarelas e brancas.',
  fr: 'La mer vue entre les troncs de deux arbres, un voilier à l’horizon ; une ombrelle '
    + 'blanche monte de la plage par un chemin entre des fleurs jaunes et blanches.',
  de: 'Das Meer zwischen den Stämmen zweier Bäume, ein Segelboot am Horizont; ein weißer '
    + 'Sonnenschirm kommt vom Strand einen Weg zwischen gelben und weißen Blumen herauf.',
  it: 'Una strada di Settignano: la bottega del pane e del vino, una donna seduta sulla '
    + 'soglia, un gatto sul selciato al sole.' }) };
const resources = [
  painting,
  // ::resource places each ornament once, so each chapter end has its own id.
  ...t({ en: ['I', 'II'], pt: ['I', 'II', 'III'], fr: ['I', 'II'], de: ['I', 'II'], it: ['I', 'II'] })
    .map((n, i) => svg(`tailpiece-${i + 1}`, 'fleuron.svg', 40, 12,
      t({ en: `A camomile flower between two sprigs closes chapter ${n}.`,
        pt: `Uma flor de camomila entre dois ramos fecha o capítulo ${n}.`,
        fr: `Une fleur de camomille entre deux brins clôt le chapitre ${n}.`,
        de: `Eine Kamillenblüte zwischen zwei Zweigen schließt das Kapitel ${n}.`,
        it: `Un fiore di camomilla tra due rametti chiude il capitolo ${n}.` }))),
];
// #endregion

const card = { id: 'card', fontFamily: LABEL, fontWeight: 600, // the fr edition's visiting card
  textAlign: 'center', firstLineIndent: pt(0) };

const config = () => ({
  locale: t({ en: 'en-us', pt: 'pt', fr: 'fr', de: 'de', it: 'it' }), // each edition's hyphenation patterns
  colorPalette,
  resourceTypes: [ornament],
  page,
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'),
    firstLineIndent: mm(4), indentAfterHeading: false,
    minWordSpacing: 0.7, maxWordSpacing: 1.9, // at the default 0.6 a line closes to 0.62
  },
  // The designs paint every heading; the level's own face is the numeral's (500 italic), so the
  // kit has no unused face to load.
  headings: { fontFamily: DISPLAY, fontWeight: 500, levels: [chapter] },
  headingStyles: [cover],
  paragraphStyles: [colophon, card],
  header,
  footer,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Pinocchio"
author: "Carlo Collodi"
---

# Pinocchio {style="cover"}

:::paragraphs{style="colophon"}
*Le avventure di Pinocchio* uscì in volume da Felice Paggi, a Firenze, nel 1883. I capitoli I e II seguono questa prima edizione, trascritta su Wikisource, con la sua grafia.

Composto in Crimson Pro, Cormorant Garamond e Cormorant SC (SIL Open Font License). In copertina, un particolare di *Settignano* (1880 circa) di Telemaco Signorini.

Un’edizione del Ricettario di Postext. Il testo è di pubblico dominio.
:::

# I {lead="— 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.

:::space

::resource{id="tailpiece-1"}

# II {lead="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.

— Buon giorno, mastr’Antonio, — disse Geppetto. — Che cosa fate costì per terra?

— Insegno l’abbaco alle formicole.

— Buon pro vi faccia.

— Chi vi ha portato da me, compar Geppetto?

— Le gambe. Sappiate, mastr’Antonio, che son venuto da voi, per chiedervi un favore.

— Eccomi qui, pronto a servirvi, — replicò il falegname rizzandosi su i ginocchi.

— Stamani m’è piovuta nel cervello un’idea.

— Sentiamola.

— Ho pensato di fabbricarmi da me un bel burattino di legno: ma un burattino maraviglioso, che sappia ballare, tirar di scherma e fare i salti mortali. Con questo burattino voglio girare il mondo, per buscarmi un tozzo di pane e un bicchier di vino: che ve ne pare?

— Bravo Polendina! — gridò la solita vocina, che non si capiva di dove uscisse.

A sentirsi chiamar Polendina, compar Geppetto diventò rosso come un peperone dalla bizza, e voltandosi verso il falegname, gli disse imbestialito:

— Perchè mi offendete?

— Chi vi offende?

— Mi avete detto Polendina!

— Non sono stato io.

— Sta’ un po’ a vedere che sarò stato io! Io dico che siete stato voi.

— No!

— Sì!

— No!

— Sì! —

E riscaldandosi sempre più, vennero dalle parole ai fatti, e acciuffatisi fra di loro, si graffiarono, si morsero e si sbertucciarono.

Finito il combattimento, mastr’Antonio si trovò fra le mani la parrucca gialla di Geppetto, e Geppetto si accòrse di avere in bocca la parrucca brizzolata del falegname.

— Rendimi la mia parrucca! — gridò mastr’Antonio.

— E tu rendimi la mia, e rifacciamo la pace. —

I due vecchietti, dopo aver ripreso ognuno di loro la propria parrucca, si strinsero la mano e giurarono di rimanere buoni amici per tutta la vita.

— Dunque, compar Geppetto, — disse il falegname in segno di pace fatta — qual è il piacere che volete da me?

— Vorrei un po’ di legno per fabbricare il mio burattino; me lo date?

Mastr’Antonio, tutto contento, andò subito a prendere sul banco quel pezzo del legno che era stato cagione a lui di tante paure. Ma quando fu lì per consegnarlo all’amico, il pezzo di legno dette uno scossone, e sgusciandogli violentemente dalle mani, andò a battere con forza negli stinchi impresciuttiti del povero Geppetto.

— Ah! gli è con questo bel garbo, mastr’Antonio, che voi regalate la vostra roba? M’avete quasi azzoppito!...

— Vi giuro che non sono stato io!

— Allora sarò stato io!...

— La colpa è tutta di questo legno....

— Lo so che è del legno: ma siete voi che me l’avete tirato nelle gambe!

— Io non ve l’ho tirato!

— Bugiardo!

— Geppetto, non mi offendete: se no vi chiamo Polendina!...

— Asino!

— Polendina!

— Somaro!

— Polendina!

— Brutto scimmiotto!

— Polendina! —

A sentirsi chiamar Polendina per la terza volta, Geppetto perse il lume degli occhi, si avventò sul falegname e lì se ne dettero un sacco e una sporta.

A battaglia finita, mastr’Antonio si trovò due graffi di più sul naso, e quell’altro due bottoni di meno al giubbetto. Pareggiati in questo modo i loro conti, si strinsero la mano e giurarono di rimanere buoni amici per tutta la vita.

Intanto Geppetto prese con sè il suo bravo pezzo di legno, e ringraziato mastr’Antonio, se ne tornò zoppicando a casa.

:::space

::resource{id="tailpiece-2"}
`; // content.<lang>.md, inlined by the Cookbook

// #region art: the fleuron, drawn in millimetres in the page's colour
function drawFleuron(color) { // a camomile head between two leafy sprigs
  const petals = Array.from({ length: 10 }, (_, i) => `<ellipse cx="22.1" cy="6" rx="1.55" `
    + `ry="0.62" transform="rotate(${i * 36} 20 6)" fill="${color}"/>`).join('');
  const sprig = `<path d="M17.2 6.3C14 7.4 10 7.2 6.4 5.6C5.2 5.1 4.2 5.2 3.4 5.9" fill="none" `
    + `stroke="${color}" stroke-width="0.55" stroke-linecap="round"/>`
    + `<path d="M13.4 6.9C12.6 4.9 10.8 4 9 4.2C10 5.6 11.4 6.6 13.4 6.9Z" fill="${color}"/>`
    + `<path d="M9.2 6.5C8.7 7.9 7.4 8.7 5.9 8.8C6.6 7.5 7.7 6.7 9.2 6.5Z" fill="${color}"/>`
    + `<circle cx="2.8" cy="6.3" r="0.7" fill="${color}"/>`;
  return '<svg xmlns="http://www.w3.org/2000/svg" width="40mm" height="12mm" '
    + `viewBox="0 0 40 12">${petals}<circle cx="20" cy="6" r="1.05" fill="${color}"/>${sprig}`
    + `<g transform="translate(40 0) scale(-1 1)">${sprig}</g></svg>`;
}
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // every face the pages use, loaded before the build
  'Crimson Pro': ['400', '400i'], // text, colophon, folios
  'Cormorant Garamond': ['500i'], // numerals, cover title, the recto's running head
  'Cormorant SC': ['600'], // the leads, the author on the cover and the verso
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await loadImage(painting.bitmap.fileId, asset(painting.bitmap.fileId));
await loadSvg('fleuron.svg', drawFleuron(palette.rubric));
const doc = await buildDocumentWithFonts({ markdown, resources }, config(), kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Trade paperback: sunk openers and recto chapters',
  pt: 'Romance em brochura: aberturas rebaixadas em página ímpar',
  fr: 'Roman broché : ouvertures basses et chapitres au recto',
  de: 'Taschenbuch: tiefe Auftakte und Kapitel rechts',
  it: 'Romanzo in brossura: aperture abbassate e capitoli sul recto' }) });

// ─── 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

### Lascia che il capitolo II segua nella pagina successiva

Con la parità `'any'`, il capitolo II si apre a pagina 6, un verso, e il libro finisce a pagina 8; il folio al piede è centrato sulla gabbia, non sulla pagina, quindi anche su quel verso sta sotto il centro del testo.

```diff
-const chapter = { level: 1, breakBefore: { enabled: true, parity: 'odd' },
+const chapter = { level: 1, breakBefore: { enabled: true, parity: 'any' },
```

### Segna un cambio di scena dentro un capitolo

Messo tra due righe `:::space`, lo stesso fiore segna un cambio di scena, e il paragrafo che lo segue comincia senza rientro perché `indentAfterHeading` è disattivato. Nell’edizione inglese, collocato dopo *seemed long to him.*, il fiore cade al piede di pagina 4; il secondo spazio lì non ci sta più e viene scartato. Lo stacco aggiunge quattro righe, che spingono da solo il finalino del capitolo I a pagina 6, sotto una testatina, quindi riadatta il capitolo dopo averne aggiunto uno.

```diff
 const resources = [
+  svg('break-1', 'fleuron.svg', 40, 12, 'A scene break.'),
```

```md
That was why the morning seemed long to him.

:::space

::resource{id="break-1"}

:::space

“You are burnt beyond recognition,” he added, …
```

## Errori frequenti

- **La pagina 1 è un recto: pianifica le pagine con i numeri fisici.** La pagina 1 è una pagina destra e la pagina 2 il primo verso, quindi pianifica le doppie pagine con i numeri di pagina fisici: un’apertura su una pagina pari sta di fronte alla pagina dispari che la segue.
- **::resource{id="…"} accetta solo virgolette doppie.** Un inserimento a blocco viene riconosciuto solo come ::resource{id="…"} con virgolette doppie; qualsiasi altra forma resta nel testo come riga visibile.
- **Avviso di impaginazione: Cascata di parità** (`parityCascade`). Le interruzioni con parità si sommano e producono più di due pagine bianche di fila. Soluzione: Rivedi le interruzioni always-odd e le parità di breakBefore; per i capitoli brevi, la parità any di solito evita le pagine bianche. ([Documentazione](https://postext.dev/it/docs/configuration-text.md#interruzione-prima-del-titolo))

- Con `'always-odd'` una pagina bianca di separazione segue ogni sezione prima di qualunque riempimento per parità: nell’edizione inglese il capitolo I passerebbe a pagina 5, dopo due pagine bianche, e il libro arriverebbe a dieci pagine.
- Le pagine dell’edizione inglese sono adattate al testo. Con un margine esterno di 16 mm invece di 15,5, la pagina 7 divide *after-din-ner* a un secondo trattino e finisce una riga prima, la pagina 8 chiude un paragrafo sul frammento *spondent.*, e il secondo finalino cade su una nona pagina. Controlla il piede di ogni pagina dopo aver cambiato la giustezza, il corpo o l’abbassamento.
- Nell’edizione inglese *Mr.* e *Mrs.* sono legati al nome con uno spazio indivisibile (U+00A0). Il paragrafo che finisce con *seemed long to him.* mantiene uno spazio normale, perché legare lì il nome allarga la sua prima riga a 2,2 volte lo spazio normale tra le parole.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Testo: The Awakening (Il risveglio, 1899), capitoli I e II, nell’edizione inglese: Kate Chopin ([fonte](https://www.gutenberg.org/ebooks/160)), pubblico dominio
- Testo: Quincas Borba (1891), capitoli da I a III, nell’edizione portoghese, con la grafia aggiornata: Machado de Assis ([fonte](https://pt.wikisource.org/wiki/Quincas_Borba)), pubblico dominio
- Testo: Madame Gervaisais (1869), capitoli I e II, nell’edizione francese, con la grafia della prima edizione: Edmond et Jules de Goncourt ([fonte](https://fr.wikisource.org/wiki/Madame_Gervaisais)), pubblico dominio
- Testo: Beate und Mareile (Beate and Mareile, 1903), chapters 1 and 2 (the second abridged), the German edition’s text, in the spelling of S. Fischer’s edition: Eduard von Keyserling ([fonte](https://de.wikisource.org/wiki/Beate_und_Mareile)), pubblico dominio
- Testo: Le avventure di Pinocchio (Felice Paggi, Firenze, 1883), capitoli I e II, con la grafia della prima edizione, senza i sommari dei capitoli: Carlo Collodi ([fonte](https://it.wikisource.org/wiki/Le_avventure_di_Pinocchio)), pubblico dominio
- Testo: Il colophon: Postext Cookbook, originale
- Immagini: La vista del mare della copertina, un dipinto: Generated With Diffusion Models, originale
- Immagini: La copertina dell’edizione italiana: un particolare di Settignano (1880 circa), olio su tela; scansione da Wikimedia Commons, un ritaglio di 1744 × 1669 px ridotto a 1190 px di larghezza: Telemaco Signorini ([fonte](https://commons.wikimedia.org/wiki/File:Settignano_(c.1880),_by_Telemaco_Signorini.jpg)), pubblico dominio
- Immagini: Il finalino alla camomilla, disegnato in codice con la tavolozza della pagina: Ignacio Ferro, MIT
- Caratteri: Crimson Pro (OFL-1.1), Cormorant Garamond (OFL-1.1), Cormorant SC (OFL-1.1)
- Codice: MIT · Contenuto di esempio: MIT

## Ricette correlate

- [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º 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º 006 · Pagine preliminari in numeri romani, poi pagina 1](https://postext.dev/it/cookbook/front-matter-roman-to-arabic.md): Copertina e preliminari sono titoli non numerati contati in romano minuscolo; :::numbering riparte da 1 sul recto dove si apre il romanzo. · Livello 3 (Avanzato) · Narrativa, teatro e prosa letteraria
