# Versi figurati: Easter Wings di Herbert, ogni riga centrata

> Easter Wings e The Altar di George Herbert composti come testo centrato, un paragrafo per ogni verso, così le lunghezze delle righe disegnano le due figure.

- Versione HTML: https://postext.dev/it/cookbook/shaped-verse
- Ricetta Nº 062 · Caratteri e testo · Livello 1 (Base) · Output: Canvas
- Generi: Poesia
- Richiede postext ≥ 1.25.0 · provata con 1.31.0 il 2026-10-13
- Pagine: [1](https://postext.dev/cookbook/shaped-verse/en/p01.webp?v=f9f3fdde), [2](https://postext.dev/cookbook/shaped-verse/en/p02.webp?v=f9f3fdde), [3](https://postext.dev/cookbook/shaped-verse/en/p03.webp?v=f9f3fdde), [4](https://postext.dev/cookbook/shaped-verse/en/p04.webp?v=f9f3fdde)
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=shaped-verse&lang=en (.postext: https://postext.dev/cookbook/shaped-verse/en/shaped-verse.postext)
- Ultimo aggiornamento: 2026-10-11
- Altre lingue: [en](https://postext.dev/en/cookbook/shaped-verse.md), [es](https://postext.dev/es/cookbook/shaped-verse.md), [ca](https://postext.dev/ca/cookbook/shaped-verse.md), [de](https://postext.dev/de/cookbook/shaped-verse.md), [fr](https://postext.dev/fr/cookbook/shaped-verse.md), [pt](https://postext.dev/pt/cookbook/shaped-verse.md), [zh](https://postext.dev/zh/cookbook/shaped-verse.md), [ja](https://postext.dev/ja/cookbook/shaped-verse.md), [ar](https://postext.dev/ar/cookbook/shaped-verse.md)

## In breve

Due antiche poesie inglesi i cui versi sono stampati in modo che ogni poesia sembri un’immagine: un altare e un paio d’ali. Ogni verso è semplicemente centrato, e la sua lunghezza disegna la forma.

## Che cosa comporrai

Un opuscolo di quattro pagine A5, un foglio A4 piegato una volta, con due carmi figurati da *The Temple* di George Herbert nella grafia della prima edizione del 1633. Il frontespizio segue quello del 1633, fino all’epigrafe dal Salmo 29. All’interno, *The Altar* sta di fronte a *Easter Wings*. Nessun verso delle due poesie è rientrato o riempito di spazi; ciascuno è centrato, così l’altare poggia su un pilastro stretto e ogni strofa delle ali si restringe fino a due parole e si riallarga. Nell’ultima pagina, sotto il motto di Herbert, una nota racconta come gli stampatori del 1633 composero le ali di traverso, e il colophon si accorcia riga dopo riga. Tutto è composto come testo corrente o come uno dei tre livelli di titolo nei caratteri IM Fell, senza stili con nome e senza disegni di pagina. L’edizione spagnola compone una versione verso per verso che conserva entrambe le forme.

**Questa ricetta risponde a:**

- Come spazio un frontespizio e le strofe di una poesia, se le righe vuote nel Markdown non aggiungono spazio?
- Come compongo poesia: una riga per verso, spazi tra le strofe, rientri sporgenti per i versi che vanno a capo, niente sillabazione?
- Come compongo un’epigrafe, una dedica, una firma o una citazione in evidenza con una grande virgoletta?
- Come forzo un’interruzione di pagina o di colonna, e faccio cominciare ogni capitolo su una pagina destra?

## La risposta breve

```js
// script.js, righe 26–38
// Postext joins the lines of a Markdown paragraph, so every line of the poem is a paragraph
// of its own, with a blank line after it. Centred, each line keeps its natural width on the
// axis of the measure, and the lengths Herbert gave his lines draw the figure:
//   Lord, who createdst man in wealth and store,
//
//   Though foolishly he lost the same,
//
//   Decaying more and more,
const verse = {
  fontFamily: 'IM Fell English', fontSize: pt(12.4), lineHeight: pt(LEAD), color: col('ink'),
  textAlign: 'center', // ragged on both sides; only justified text is hyphenated by default
  firstLineIndent: pt(0), // the default 1.5 em moves every line 3.3 mm off the titles' axis
};
```

## Ingredienti

**Insegna**

- [Rientri, allineamento e spaziatura dei paragrafi](https://postext.dev/it/docs/configuration-text.md#testo-corrente): Testo giustificato o in bandiera, rientri di prima riga con il primo paragrafo a filo dopo un titolo, o paragrafi a blocchi separati da spazio.
- [Livelli dei titoli](https://postext.dev/it/docs/configuration-text.md#sovrascritture-per-livello): Carattere, scala dei corpi, colore, peso, margini, corsivo e maiuscole per ogni livello di titolo, da H1 a H6.
- [Spazio verticale esplicito](https://postext.dev/it/docs/document-format.md#space): Aggiunge righe intere o frazioni di riga di spazio tra due blocchi, dove le righe vuote non aggiungono nulla; viene eliminato in cima a una colonna.

**Usa anche**

- [Interruzioni di pagina e di colonna](https://postext.dev/it/docs/document-format.md#pagebreak)
- [Capitoli che si aprono su un recto](https://postext.dev/it/docs/configuration-text.md#interruzione-prima-del-titolo)
- [Carattere del testo corrente](https://postext.dev/it/docs/configuration-text.md#testo-corrente)
- [Grassetto, corsivo e i loro colori](https://postext.dev/it/docs/configuration-text.md#testo-corrente)
- [Formato della pagina](https://postext.dev/it/docs/configuration-page-layout.md#formati-di-pagina-predefiniti)
- [Margini speculari](https://postext.dev/it/docs/configuration-page-layout.md#margini-speculari)
- [Una o due colonne](https://postext.dev/it/docs/configuration-page-layout.md#tipi-di-impaginazione)
- [Colore della carta](https://postext.dev/it/docs/configuration-page-layout.md#pagina)
- [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)

**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), [`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)

**API**

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

**Caratteri**

- IM Fell English (OFL-1.1), IM Fell Great Primer (OFL-1.1), IM Fell English SC (OFL-1.1)

## Preparazione

### 1 · Un verso è un paragrafo centrato

Il codice è [la risposta breve](https://postext.dev/it/cookbook/shaped-verse.md#la-risposta-breve) qui sopra. Postext unisce in una sola le righe di un paragrafo Markdown, quindi ogni verso è un paragrafo, con una riga vuota dopo. `textAlign: 'center'` compone ogni riga alla sua larghezza naturale sull’asse della giustezza, e poiché solo il testo giustificato viene sillabato a meno che `hyphenation.ragged` non sia attivo, nessuna parola viene divisa ([testo corrente](https://postext.dev/it/docs/configuration-text.md#testo-corrente)). Ogni riga è la prima del suo paragrafo, quindi il rientro di prima riga predefinito di 1,5 em la spingerebbe 3,3 mm a destra dell’asse dei titoli; `firstLineIndent: pt(0)` tiene poesie e titoli su un solo asse.

### 2 · La giustezza accoglie il verso più lungo

```js
// script.js, righe 42–47
// A line too long for the measure wraps, and both halves are centred, which adds a step to
// the outline.
// The widest line, 'Lord, who createdst man in wealth and store,', is 80.7 mm in 12.4 pt
// Fell, so the 110 mm measure takes every line of both poems whole.
const TRIM = { w: 148, h: 210 }; // mm: A5, one A4 sheet folded once
const MARGIN = { top: 22, bottom: 24, inner: 17, outer: 21 }; // mm: the measure is 110 mm
```

Una riga che non ci sta va a capo, ed entrambe le metà sono centrate, così il profilo acquista un gradino che non c’è nella figura di Herbert. Con una giustezza di 75 mm, tre versi di *The Altar* e due di *Easter Wings* vanno a capo, e l’opuscolo arriva a sei pagine. Il verso più largo qui, il primo di *Easter Wings*, misura 80,7 mm in inglese e 80,0 mm in spagnolo, quindi la giustezza di 110 mm della pagina A5 accoglie ogni verso intero.

### 3 · Titoli semplici, centrati

```js
// script.js, righe 51–71
// headings.textAlign centres every level. The Fell faces ship weight 400 only, so no level
// asks for bold.
const titles = {
  fontFamily: 'IM Fell Great Primer', fontWeight: 400, color: col('violet'), textAlign: 'center',
  levels: [
    // The keepsake's title opens page 1, where a break changes nothing; a second # title
    // would open on a recto.
    { level: 1, fontSize: pt(40), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      marginBottom: pt(LEAD), breakBefore: { enabled: true, parity: 'odd' } },
    // A poem opens the next page, recto or verso: The Altar (page 2) faces Easter Wings.
    // A heading that opens a page drops its top margin, so the title is lowered by its
    // line box instead: three lines deep, it sets the title about 5 mm down. The default
    // half-em margin under it rounds up to a whole line, and each poem starts on line 5.
    { level: 2, fontSize: pt(24), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      breakBefore: { enabled: true, parity: 'any' } },
    // The small-capital labels on pages 1 and 4: the sister face at the body size, with no
    // margins, since the :::space lines place them.
    { level: 3, fontFamily: 'IM Fell English SC', fontSize: pt(12.4), lineHeight: pt(LEAD),
      marginTop: pt(0), marginBottom: pt(0) },
  ],
};
```

Nessun livello ha un disegno, solo impostazioni tipografiche, e `headings.textAlign` centra tutti e tre ([titoli](https://postext.dev/it/docs/configuration-text.md#titoli)). La parità `'any'` apre ogni poesia sulla pagina successiva, recto o verso, così l’altare a pagina 2 sta di fronte alle ali a pagina 3. Un `marginTop` non può abbassare quei titoli, perché lo spazio sopra un titolo che apre una pagina viene eliminato. È invece il riquadro di riga del livello 2, alto tre righe, ad abbassare ogni titolo di circa 5 mm, ed entrambe le poesie cominciano alla riga 5 della gabbia del testo. Il livello 1 apre il documento e qui non ha bisogno di un’interruzione; ne porta comunque una, perché un secondo titolo `#` si apra su un recto. Il livello 3 compone le quattro etichette delle pagine 1 e 4 nel maiuscoletto di IM Fell English SC, al corpo del testo.

### 4 · Il Markdown spazia e interrompe le pagine

```js
// script.js, righe 75–92
// A blank line adds no space, so every gap on these pages is a :::space{lines=N}, which adds
// N lines of the 17 pt grid (one without the attribute). ## opens a page for each poem, and
// :::pagebreak opens the last page.
const config = () => ({
  colorPalette,
  locale: t({ en: 'en-us', es: 'es' }), // the language of each edition, and of its bundle
  page: { // mirror: left is the inner margin; 150 dpi is for the screen
    sizePreset: 'custom', width: mm(TRIM.w), height: mm(TRIM.h), dpi: 150,
    backgroundColor: col('paper'),
    margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner),
      right: mm(MARGIN.outer), mirror: true },
  },
  layout: { layoutType: 'single' }, // the default is two columns
  bodyText: verse, // a :ref added to these pages takes main-color, the ink, like bold and italic
  headings: titles,
  header: { elements: [] }, // a folded keepsake of four pages: no running heads,
  footer: { elements: [] }, // and no folios
});
```

Una riga vuota tra i paragrafi non aggiunge spazio, quindi ogni spazio di queste pagine viene da `:::space`: una riga tra le strofe di *Easter Wings*, e nel frontespizio cinque righe sopra l’etichetta del Salmo e dieci sotto l’epigrafe ([`:::space`](https://postext.dev/it/docs/document-format.md#space)). Con questi conteggi le note tipografiche di pagina 1 finiscono sull’ultima riga della gabbia del testo, e le etichette sopra il Salmo e sopra la nota stanno entrambe alla riga 13, a 94 mm dalla sommità della pagina. `:::pagebreak` apre l’ultima pagina ([`:::pagebreak`](https://postext.dev/it/docs/document-format.md#pagebreak)). Anche il colophon è composto un paragrafo per riga, spezzato dove finisce una frase, così le sue quattro righe si assottigliano in un finalino.

## 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/shaped-verse

### script.js

```js
// ═══ Postext Cookbook · Nº 062 · Shaped verse: Herbert's Easter Wings, centred line by line ═
// https://postext.dev/en/cookbook/shaped-verse
// Code: MIT · Text: G. Herbert, The Temple, 1633 (PD; EEBO-TCP, CC0) · Spanish version: CC BY 4.0
// Fonts: IM Fell English, Great Primer and English SC (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import { buildDocumentWithFonts, renderPageToCanvas } from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'shaped-verse';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { // every colour in the config links to one of these
  ink: '#241d29', // the text: a violet-black
  violet: '#5c2a6f', // the one accent: the titles and the small-capital labels
  paper: '#fbf7ee', // the sheet: an uncoated cream
};
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' } })),
  // Italic and bold take their colour from 'main-color' unless set. With 'main-color' in ink,
  // the italic subtitle, epigraph and motto print in ink instead of the default blue.
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
const LEAD = 17; // pt: the leading of every line, verse and prose alike

// #region answer: centred text, one line of verse to a paragraph
// Postext joins the lines of a Markdown paragraph, so every line of the poem is a paragraph
// of its own, with a blank line after it. Centred, each line keeps its natural width on the
// axis of the measure, and the lengths Herbert gave his lines draw the figure:
//   Lord, who createdst man in wealth and store,
//
//   Though foolishly he lost the same,
//
//   Decaying more and more,
const verse = {
  fontFamily: 'IM Fell English', fontSize: pt(12.4), lineHeight: pt(LEAD), color: col('ink'),
  textAlign: 'center', // ragged on both sides; only justified text is hyphenated by default
  firstLineIndent: pt(0), // the default 1.5 em moves every line 3.3 mm off the titles' axis
};
// #endregion

// #region measure: an A5 leaf whose measure holds the longest line whole
// A line too long for the measure wraps, and both halves are centred, which adds a step to
// the outline.
// The widest line, 'Lord, who createdst man in wealth and store,', is 80.7 mm in 12.4 pt
// Fell, so the 110 mm measure takes every line of both poems whole.
const TRIM = { w: 148, h: 210 }; // mm: A5, one A4 sheet folded once
const MARGIN = { top: 22, bottom: 24, inner: 17, outer: 21 }; // mm: the measure is 110 mm
// #endregion

// #region titles: plain headings, centred, in the display face and the accent
// headings.textAlign centres every level. The Fell faces ship weight 400 only, so no level
// asks for bold.
const titles = {
  fontFamily: 'IM Fell Great Primer', fontWeight: 400, color: col('violet'), textAlign: 'center',
  levels: [
    // The keepsake's title opens page 1, where a break changes nothing; a second # title
    // would open on a recto.
    { level: 1, fontSize: pt(40), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      marginBottom: pt(LEAD), breakBefore: { enabled: true, parity: 'odd' } },
    // A poem opens the next page, recto or verso: The Altar (page 2) faces Easter Wings.
    // A heading that opens a page drops its top margin, so the title is lowered by its
    // line box instead: three lines deep, it sets the title about 5 mm down. The default
    // half-em margin under it rounds up to a whole line, and each poem starts on line 5.
    { level: 2, fontSize: pt(24), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      breakBefore: { enabled: true, parity: 'any' } },
    // The small-capital labels on pages 1 and 4: the sister face at the body size, with no
    // margins, since the :::space lines place them.
    { level: 3, fontFamily: 'IM Fell English SC', fontSize: pt(12.4), lineHeight: pt(LEAD),
      marginTop: pt(0), marginBottom: pt(0) },
  ],
};
// #endregion

// #region pages: four pages, spaced and broken from the Markdown
// A blank line adds no space, so every gap on these pages is a :::space{lines=N}, which adds
// N lines of the 17 pt grid (one without the attribute). ## opens a page for each poem, and
// :::pagebreak opens the last page.
const config = () => ({
  colorPalette,
  locale: t({ en: 'en-us', es: 'es' }), // the language of each edition, and of its bundle
  page: { // mirror: left is the inner margin; 150 dpi is for the screen
    sizePreset: 'custom', width: mm(TRIM.w), height: mm(TRIM.h), dpi: 150,
    backgroundColor: col('paper'),
    margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner),
      right: mm(MARGIN.outer), mirror: true },
  },
  layout: { layoutType: 'single' }, // the default is two columns
  bodyText: verse, // a :ref added to these pages takes main-color, the ink, like bold and italic
  headings: titles,
  header: { elements: [] }, // a folded keepsake of four pages: no running heads,
  footer: { elements: [] }, // and no folios
});
// #endregion

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "The Temple"
author: "George Herbert"
---

# The Temple

*Sacred Poems and Private Ejaculations*

:::space

By Mr. George Herbert

:::space{lines=5}

### Psal. 29.

*In his Temple doth every man speak of his honour.*

:::space{lines=10}

### Two shaped poems

in the text of the first edition

Cambridge, 1633

## The Altar

A broken ALTAR, Lord, thy servant reares,

Made of a heart, and cemented with teares:

Whose parts are as thy hand did frame;

No workmans tool hath touch’d the same.

A HEART alone

Is such a stone,

As nothing but

Thy pow’r doth cut.

Wherefore each part

Of my hard heart

Meets in this frame,

To praise thy name.

That if I chance to hold my peace,

These stones to praise thee may not cease.

O let thy blessed SACRIFICE be mine,

And sanctifie this ALTAR to be thine.

## Easter Wings

Lord, who createdst man in wealth and store,

Though foolishly he lost the same,

Decaying more and more,

Till he became

Most poore:

With thee

O let me rise

As larks, harmoniously,

And sing this day thy victories:

Then shall the fall further the flight in me.

:::space

My tender age in sorrow did beginne:

And still with sicknesses and shame

Thou didst so punish sinne,

That I became

Most thinne.

With thee

Let me combine,

And feel this day thy victorie:

For, if I imp my wing on thine,

Affliction shall advance the flight in me.

:::pagebreak

### Herbert’s motto

*Lesse then the least of Gods mercies.*

:::space

With it, his printers wrote, he used to conclude “all things that might seem to tend any way to his own honour”.

:::space{lines=7}

### A note on the text

The poems keep the spelling of the first edition, printed at Cambridge in 1633. There “The Altar” stands upright on page 18, and the two stanzas of “Easter Wings” lie sideways on pages 34 and 35. Here they stand upright; give the booklet a quarter turn and each opens into a pair of wings.

:::space{lines=5}

Set in IM Fell English, English SC and Great Primer,

Igino Marini’s revivals of the types

John Fell gathered at Oxford.

Text from EEBO-TCP.
`; // content.<lang>.md, inlined by the Cookbook

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
// Every face the design uses. Layout measures with the browser's fonts, so the
// kit loads them from Fontsource before the first build.
const FONTS = {
  'IM Fell English': ['400', '400i'],
  'IM Fell Great Primer': ['400'],
  'IM Fell English SC': ['400'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const doc = await buildDocumentWithFonts({ markdown }, config(), kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Easter Wings and The Altar', es: 'Alas de Pascua y El altar' }) });

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

## Varianti

### Apri ogni poesia su una pagina destra

Con la parità `'odd'` *The Altar* ed *Easter Wings* si aprono alle pagine 3 e 5, entrambe recto, e l’opuscolo arriva a sei pagine con le pagine 2 e 4 bianche.

```diff
-      breakBefore: { enabled: true, parity: 'any' } },
+      breakBefore: { enabled: true, parity: 'odd' } },
```

### Allinea i versi a sinistra

Con `textAlign: 'left'` ogni riga comincia sul margine e le due poesie conservano solo il profilo destro; anche il resto del testo corrente, dal sottotitolo al colophon, va allineato a sinistra, e restano centrati solo i titoli.

```diff
-  textAlign: 'center', // ragged on both sides; only justified text is hyphenated by default
+  textAlign: 'left', // ragged right; only justified text is hyphenated by default
```

## Errori frequenti

- Un verso troppo lungo per la giustezza va a capo, ed entrambe le metà vengono centrate ciascuna per conto suo. Misura il verso più largo prima di scegliere il corpo e i margini. Per versi allineati a sinistra, con righe a capo sporgenti, vedi [Poesie composte verso per verso](https://postext.dev/it/cookbook/poetry-collection.md).
- Gli spazi iniziali vengono tolti da un paragrafo, quindi non puoi dare forma a una riga riempiendola di spazi. La larghezza di ogni riga dipende solo dalle sue parole, e una traduzione deve trovare versi larghi più o meno quanto quelli dell’originale, come fa l’edizione spagnola.
- La centratura tiene ogni riga alla sua larghezza naturale, quindi la lastra e la base di *The Altar* non risultano squadrate: la terza e la quarta riga della lastra sono più strette delle prime due. Nemmeno la giustificazione può squadrarle, perché l’ultima riga di un paragrafo giustificato mantiene la sua larghezza naturale e ognuna di queste righe è un paragrafo a sé.
- Il frontespizio e l’ultima pagina sono pieni fino all’ultima riga della griglia. Una riga in più sul frontespizio, per un’epigrafe più lunga, una riga che va a capo o un `:::space` più grande, sposta l’etichetta al piede e le due righe sotto di essa su una pagina tutta loro; una riga in più nell’ultima pagina sposta da sola l’ultima riga del colophon su una quinta pagina. Per aggiungere una riga a una delle due pagine, toglila da un `:::space` sopra di essa.
- Postext non compone il testo di traverso, come gli stampatori del 1633 composero *Easter Wings*: ruotano solo figure e tabelle intere. Qui le strofe stanno in piedi, e la nota a pagina 4 lo dice.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Testo: “The Altar” and “Easter wings”, the title, subtitle, byline and epigraph of the title page, and Herbert’s motto with a clause of the printers’ preface, from the first edition of The Temple (Cambridge, 1633) in the EEBO-TCP transcription (CC0 1.0); the letters and stops it marks as illegible or unknown at six line ends (four in The Altar, two in Easter wings) are supplied: George Herbert; Text Creation Partnership ([fonte](https://github.com/textcreationpartnership/A03058)), pubblico dominio
- Testo: Psalm 29:9 in the Spanish edition, from the Reina-Valera Bible of 1909: Reina-Valera 1909 ([fonte](https://es.wikisource.org/wiki/Biblia_Reina-Valera_1909/Salmos/29)), pubblico dominio
- Testo: The Spanish versions of both poems, of the subtitle, the motto and the printers’ clause, and the title-page imprint, the note on the text and the colophon of both editions, written for this recipe: Ignacio Ferro, CC-BY-4.0
- Caratteri: IM Fell English (OFL-1.1), IM Fell Great Primer (OFL-1.1), IM Fell English SC (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 015 · Poesie composte verso per verso](https://postext.dev/it/cookbook/poetry-collection.md): Ogni poesia è un blocco :::verse scritto come stampato: i rientri del 1918 sono spazi iniziali, i versi a capo sporgono di 2 em, righe vuote tra le strofe. · Livello 2 (Intermedio) · Poesia
- [Nº 043 · Traduzione a fronte, strofa per strofa](https://postext.dev/it/cookbook/bilingual-facing-verse.md): Ogni poesia è un riquadro senza cornice a due colonne: un salto di colonna fisso apre l’inglese sul suo titolo e :::space allinea ogni strofa all’originale. · Livello 2 (Intermedio) · Poesia
- [Nº 044 · Edizione critica: numeri di verso e note per verso](https://postext.dev/it/cookbook/critical-edition-line-numbers.md): Il Lycidas di Milton nel testo del 1645: lineNumbers stampa un numero ogni cinque versi nella colonna laterale, e ogni nota si apre su quel numero, come chip. · Livello 3 (Avanzato) · Poesia
