# Una obra col·lectiva amb bibliografia a cada capítol

> Tres assajos de tres autors, cadascun un document propi a buildBundle, i cadascun tancat per una llista Chicago autor-data amb només les obres que cita.

- Versió HTML: https://postext.dev/ca/cookbook/edited-volume-chapter-bibliographies
- Recepta Núm. 098 · Estructura del llibre · Nivell 3 (Avançat) · Sortides: Canvas, PDF
- Gèneres: Articles i treballs acadèmics, Llibres de text
- Requereix postext ≥ 1.12.1, postext-pdf ≥ 1.12.1 · provada amb 1.12.1, postext-pdf 1.12.1 el 2026-10-01
- Pàgines: [1](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p01.webp?v=ddb852a9), [2](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p02.webp?v=ddb852a9), [3](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p03.webp?v=ddb852a9), [4](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p04.webp?v=ddb852a9), [5](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p05.webp?v=ddb852a9), [6](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p06.webp?v=ddb852a9), [7](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p07.webp?v=ddb852a9), [8](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p08.webp?v=ddb852a9), [9](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p09.webp?v=ddb852a9), [10](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/p10.webp?v=ddb852a9)
- PDF: https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/edited-volume-chapter-bibliographies.pdf?v=ddb852a9
- Obre al Sandbox: https://postext.dev/ca/sandbox#recipe=edited-volume-chapter-bibliographies&lang=es (.postext: https://postext.dev/cookbook/edited-volume-chapter-bibliographies/es/edited-volume-chapter-bibliographies.postext)
- Última actualització: 2026-10-01
- Altres idiomes: [en](https://postext.dev/en/cookbook/edited-volume-chapter-bibliographies.md), [es](https://postext.dev/es/cookbook/edited-volume-chapter-bibliographies.md), [zh](https://postext.dev/zh/cookbook/edited-volume-chapter-bibliographies.md), [ar](https://postext.dev/ar/cookbook/edited-volume-chapter-bibliographies.md)

## En poques paraules

Un llibre d'assajos d'autors diferents. Cada assaig porta el nom del seu autor sota el títol i acaba amb la seva pròpia llista de referències, com a les obres col·lectives.

## Què compondràs

*La página en obra*, una petita col·lecció d'assajos sobre història de la impremta, amb dos editors i tres autors. Cada autor lliura el seu assaig en un fitxer Markdown amb el seu propi bloc BibTeX, i així el conserva el llibre: un volum de 155 × 235 mm en Libre Caslon, una portada vermella i capítols que s'obren amb el número, el títol i el nom de l'autor. Cada capítol es tanca amb la seva llista de Referències en Chicago autor-data, que recull només les obres citades en aquell assaig. Dos autors citen *The Printing Press as an Agent of Change*, d'Eisenstein, i l'obra apareix a les dues llistes; amb la *New Introduction to Bibliography* de Gaskell passa el mateix als capítols 1 i 3.

**Aquesta recepta respon a:**

- Com dono a cada capítol d'una obra col·lectiva la seva signatura i la seva pròpia llista de referències?
- Com cito obres i componc la bibliografia en APA, IEEE o un altre estil de cita?

## La resposta curta

```js
// script.js, línies 39–55
// Citations are [@key] in the text; each contributor's BibTeX goes in a :::references block
// in their own chapter. Register the engine once, before the first build.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'chicago-author-date', // the humanities' author-date: (Eisenstein 1979)
  bibliography: {
    // 'chapter': a :::bibliography lists the works its own document cites, so a work two
    // contributors cite appears in both lists. 'book' (the default) would print one list,
    // after the last chapter. The scope is per document, not per H1: each essay must be a
    // document of its own in buildBundle (see the chapters region).
    scope: 'chapter',
    title: '', // no bold title paragraph: each chapter has its own ## References heading
    fontSize: em(0.9), lineHeight: pt(12.4), hangingIndent: mm(5), entrySpacing: pt(2.6),
  },
};
// Numbering and disambiguation still see the whole book: two Smiths of 2001 in different
// chapters would print 2001a and 2001b, as one bibliography would.
```

## Ingredients

**Ensenya**

- [Bibliografia a partir de les referències](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia): La llista d'obres citades, construïda a partir de les referències del document (front matter o un bloc BibTeX) on hi ha :::bibliography o després de l'últim capítol.
- [Cites en un estil de cita](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia): Obres citades com a [@clau, pàg. 33] i formatades en un estil CSL (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…) triat a la configuració, enllaçades amb les seves entrades.

**També fa servir**

- [Llibres construïts capítol a capítol](https://postext.dev/ca/docs/configuration.md#compondre-i-renderitzar-un-paquet)
- [Atributs de títol](https://postext.dev/ca/docs/document-format.md#atributs-dencapçalament)
- [Obertures dissenyades](https://postext.dev/ca/docs/configuration.md#span-i-disseny-avançat)
- [Índex de continguts](https://postext.dev/ca/docs/configuration.md#índex-de-continguts)
- [Capçaleres i folis](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus)
- [Capçaleres segons el tipus de pàgina](https://postext.dev/ca/docs/configuration.md#elements-de-text)
- [Estils de títol](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament)
- [Capítols sense número](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament)
- [Cobertes, portades i colofons](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament)
- [Ancoratge d'elements de disseny](https://postext.dev/ca/docs/configuration.md#posicionament-delements)
- [Requadres flotants](https://postext.dev/ca/docs/configuration.md#el-contenidor-callout)
- [Marges simètrics](https://postext.dev/ca/docs/configuration.md#marges-simètrics-mirall)
- [Paleta de color semàntica](https://postext.dev/ca/docs/configuration.md#paleta-de-colors)
- [Exportació a PDF](https://postext.dev/ca/docs/configuration.md#generació-de-pdf)
- [Requadres](https://postext.dev/ca/docs/configuration.md#estils-davís)
- [Equilibri de columnes](https://postext.dev/ca/docs/configuration.md#equilibratge-de-columnes)
- [Títols numerats](https://postext.dev/ca/docs/configuration.md#configuració-per-nivell)
- [Color del paper](https://postext.dev/ca/docs/configuration.md#pàgina)
- [Fonts incrustades al PDF](https://postext.dev/ca/docs/configuration.md#per-què-un-proveïdor-de-fonts)
- [Capçaleres per secció](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament)

**La configuració d'un cop d'ull**

- [`bodyText`](https://postext.dev/ca/docs/configuration.md#text-de-cos), [`calloutStyles`](https://postext.dev/ca/docs/configuration.md#estils-davís), [`citations`](https://postext.dev/ca/docs/configuration.md#cites), [`colorPalette`](https://postext.dev/ca/docs/configuration.md#paleta-de-colors), [`footer`](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus), [`header`](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus), [`headingStyles`](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament), [`headings`](https://postext.dev/ca/docs/configuration.md#encapçalaments), [`layout`](https://postext.dev/ca/docs/configuration.md#disposició), [`locale`](https://postext.dev/ca/docs/configuration.md#partició-de-mots), [`page`](https://postext.dev/ca/docs/configuration.md#pàgina), [`toc`](https://postext.dev/ca/docs/configuration.md#índex-de-continguts)

**API**

- [`LOCALES`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`STYLES`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`buildBundle`](https://postext.dev/ca/docs/configuration.md#compondre-i-renderitzar-un-paquet), [`clearMeasurementCache`](https://postext.dev/ca/docs/configuration.md#memòria-cau-de-mesures), [`createCiteprocEngine`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`decompressWoff2`](https://postext.dev/ca/docs/configuration.md#proveïdor-de-fonts-al-navegador-fontsource--woff2), [`registerCitationEngine`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`renderPageToCanvas`](https://postext.dev/ca/docs/configuration.md#renderitzar-una-pàgina-a-un-bitmap), [`renderToPdf`](https://postext.dev/ca/docs/configuration.md#generació-de-pdf)

**Tipus de lletra**

- Libre Caslon Text (OFL-1.1), Libre Caslon Display (OFL-1.1), IBM Plex Sans Condensed (OFL-1.1)

## Elaboració

### 1 · Un document per autor

```js
// script.js, línies 334–337
// A chapter's :::bibliography lists what that document cites; one Markdown file holding all
// three essays would make every list repeat the works of the chapters before it.
const chapters = [front, lowe, okafor, ferrer].map((markdown) => ({ markdown }));
const book = () => buildBundle({ chapters, config: config(), resources: [] });
```

L'abast de [la resposta curta](#la-resposta-curta) és el d'un document. `buildBundle` compon els quatre documents com un sol llibre, de manera que els folis, els números de capítol i l'índex segueixen la seqüència, mentre que cada `:::bibliography` reuneix les citacions del seu propi fitxer. Les referències es queden on les va escriure cada autor. Una clau definida en qualsevol capítol val per a tot el llibre, així que n'hi hauria hagut prou amb una sola entrada compartida, però amb capítols autònoms pots canviar l'ordre o substituir un assaig sense trencar la llista d'un altre.

### 2 · La signatura és un atribut del títol

```js
// script.js, línies 59–65
// # The Compositor's Day {author="Margaret Lowe" short="The Compositor's Day"}
const byline = text('byline', '{attr.author}', { fontFamily: TEXT, italic: true,
  fontSize: pt(12), color: col('ink'), align: 'left' }, below('title', 4));
const versoAuthor = head('verso-author', '{attr.author}', 'even',
  at('page', 'top-left', MARGIN.outer + 8, 13)); // in the running head
const tocAuthor = { enabled: true, attr: 'author', fontFamily: TEXT, fontSize: pt(9.5),
  color: col('muted') }; // a line under each title in the contents (italic by default)
```

El títol de cada assaig porta `author="…"` i un títol curt, `short="…"`, per a la capçalera. L'obertura imprimeix l'autor sota el títol, la capçalera de la pàgina parella anomena l'autor de l'assaig al qual pertany la pàgina i l'índex posa el nom en una línia sota cada entrada. Els preliminars no han de repetir la llista d'autors.

### 3 · L'obertura

```js
// script.js, línies 69–78
const opener = { enabled: true, minHeight: mm(48), slot: { elements: [
  text('kicker', t({ en: 'Chapter {number}', es: 'Capítulo {number}' }), { ...label,
    fontSize: pt(8.5), letterSpacing: pt(1.7), color: col('accent') },
  at('container', 'top-left', 0, 4)),
  text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(28), lineHeight: 1.08,
    color: col('ink'), align: 'left', overflow: 'wrap' }, below('kicker', 3, { width: 'fill' })),
  byline,
  { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.2), color: col('accent'),
    placement: { ...below('byline', 6), size: { width: mm(16) } } },
] } };
```

Avanttítol, títol i signatura s'apilen des del capdamunt de la caixa, cadascun ancorat sota l'anterior, així que un títol de dues línies com el del capítol 3 fa baixar amb ell la signatura i el filet. La reserva de 48 mm admet dues línies de títol, i el text comença a la mateixa línia de la retícula a tots els capítols.

### 4 · Capçaleres per a un llibre de diversos autors

```js
// script.js, línies 82–96
function head(id, content, parity, placement, look = {}) { // hoisted: the byline uses it
  return { ...text(id, content, { ...label, fontWeight: 500, fontSize: pt(7.5),
    letterSpacing: pt(1.1), color: col('muted'), ...look }, placement), parity, pages: 'body' };
}
const folio = { fontWeight: 600, color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, 13), folio),
  versoAuthor,
  head('recto-title', '{attr.short}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), 13),
    { align: 'right' }),
  head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, 13),
    { ...folio, align: 'right' }),
] };
const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all',
  at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] };
```

Una obra col·lectiva anomena l'autor a la pàgina parella i el capítol a la senar, on una monografia posaria el títol del llibre i el del capítol. Les capçaleres es limiten a `pages: 'body'`, de manera que les obertures només porten un foli al peu.

### 5 · Els preliminars

```js
// script.js, línies 100–130
const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
const cover = { enabled: true, slot: { elements: [
  { kind: 'box', id: 'field', style: { backgroundColor: col('accent') },
    placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: mm(150) } } },
  text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(58), lineHeight: 1,
    color: col('paper'), align: 'left', overflow: 'wrap' },
  { ...at('page', 'top-left', MARGIN.inner, 64), size: { width: mm(118) } }),
  text('subtitle', '{subtitle}', { fontFamily: TEXT, italic: true, fontSize: pt(15),
    color: col('paper'), align: 'left' }, below('title', 5)),
  text('editors', '{attr.editors}', { ...label, fontSize: pt(9), letterSpacing: pt(1.8),
    color: col('accent') }, at('page', 'top-left', MARGIN.inner, 162)),
  text('contributors', '{attr.contributors}', { fontFamily: TEXT, italic: true, fontSize: pt(12),
    color: col('ink'), align: 'left' }, below('editors', 3)),
  text('imprint', '{attr.imprint}', { ...label, fontWeight: 500, fontSize: pt(7.5),
    letterSpacing: pt(1.5), color: col('muted') }, at('page', 'bottom-left', MARGIN.inner, -16)),
] } };
const contentsOpener = { enabled: true, minHeight: mm(40), slot: { elements: [
  text('kicker', '{title}', { ...label, fontSize: pt(8.5), letterSpacing: pt(1.7),
    color: col('accent') }, at('container', 'top-left', 0, 4)),
  text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(28), color: col('ink'),
    align: 'left' }, below('kicker', 3)),
] } };
const contents = {
  levels: [{ level: 1, fontFamily: TEXT, fontSize: pt(12.5), lineHeight: pt(LEAD),
    numberFontFamily: LABEL, numberFontSize: pt(10), numberFontWeight: 600,
    numberColor: col('accent'), numberWidth: mm(6), numberGap: mm(3), marginBottom: pt(LEAD) }],
  pageNumber: { fontFamily: LABEL, fontSize: pt(9), fontWeight: 600, color: col('ink'),
    width: mm(7) },
  leader: { char: '. ', gap: mm(2) },
  subtitle: tocAuthor,
};
```

La portada i l'índex són dos títols sense número al primer document, sense capçaleres i sense entrada a l'índex. Els seus dissenys llegeixen `{title}` i `{subtitle}` del frontmatter, i els editors i autors, dels atributs del mateix títol.

## La recepta completa

Un sol fitxer, compost a partir de la carpeta de la recepta amb el text d'exemple i el kit comú del Receptari ja inclosos; construeix la seva pròpia pàgina. Per executar-lo, posa'l en un `<script type="module">` d'una pàgina buida o enganxa'l al tauler JS d'un pen nou de CodePen (com a mòdul). Importa postext des d'esm.sh, així que no cal instal·lar ni compilar res.

- Carpeta de la recepta: https://github.com/drnachio/postext/tree/main/cookbook/edited-volume-chapter-bibliographies

### script.js

```js
// ═══ Postext Cookbook · Nº 098 · An edited volume with a bibliography per chapter ═════
// https://postext.dev/en/cookbook/edited-volume-chapter-bibliographies
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Libre Caslon Text & Display, IBM Plex Sans Condensed (OFL 1.1) · Needs postext ≥ 1.12.1
// Three essays by three contributors, each a Markdown document of its own with its own
// BibTeX block, laid out by buildBundle as one volume: every chapter closes on its own list.
import {
  buildBundle, renderPageToCanvas, clearMeasurementCache, registerCitationEngine,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'es'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'edited-volume-chapter-bibliographies';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: printer's red on a warm paper, the rest in ink
const palette = {
  ink: '#211c19', // text: a warm near-black
  accent: '#8e2b1f', // printer's red: the cover field, kickers, the references heading
  rule: '#cdbfb2', // hairlines
  muted: '#6b5f57', // running heads, the contents' contributors, the colophon
  paper: '#fcfaf6', // the page, and the type on the red field
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.accent })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const TEXT = 'Libre Caslon Text', DISPLAY = 'Libre Caslon Display';
const LABEL = 'IBM Plex Sans Condensed';
const MARGIN = { top: 23, bottom: 22, inner: 20, outer: 18 }; // mm, mirrored
const LEAD = 13.8; // body leading in pt: the baseline grid
const label = { fontFamily: LABEL, fontWeight: 600, textTransform: 'uppercase', align: 'left' };
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const below = (id, y, size) => ({ ...at(`#${id}`, 'below', 0, y), ...(size && { size }) });
const text = (id, content, look, placement) => ({ kind: 'text', id, content, ...look, placement });

// #region answer: one list per chapter: scope 'chapter' over a book of separate documents
// Citations are [@key] in the text; each contributor's BibTeX goes in a :::references block
// in their own chapter. Register the engine once, before the first build.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'chicago-author-date', // the humanities' author-date: (Eisenstein 1979)
  bibliography: {
    // 'chapter': a :::bibliography lists the works its own document cites, so a work two
    // contributors cite appears in both lists. 'book' (the default) would print one list,
    // after the last chapter. The scope is per document, not per H1: each essay must be a
    // document of its own in buildBundle (see the chapters region).
    scope: 'chapter',
    title: '', // no bold title paragraph: each chapter has its own ## References heading
    fontSize: em(0.9), lineHeight: pt(12.4), hangingIndent: mm(5), entrySpacing: pt(2.6),
  },
};
// Numbering and disambiguation still see the whole book: two Smiths of 2001 in different
// chapters would print 2001a and 2001b, as one bibliography would.
// #endregion

// #region byline: the contributor, from the heading, in three places
// # The Compositor's Day {author="Margaret Lowe" short="The Compositor's Day"}
const byline = text('byline', '{attr.author}', { fontFamily: TEXT, italic: true,
  fontSize: pt(12), color: col('ink'), align: 'left' }, below('title', 4));
const versoAuthor = head('verso-author', '{attr.author}', 'even',
  at('page', 'top-left', MARGIN.outer + 8, 13)); // in the running head
const tocAuthor = { enabled: true, attr: 'author', fontFamily: TEXT, fontSize: pt(9.5),
  color: col('muted') }; // a line under each title in the contents (italic by default)
// #endregion

// #region opener: kicker, title and byline over a short red rule, sunk 48 mm
const opener = { enabled: true, minHeight: mm(48), slot: { elements: [
  text('kicker', t({ en: 'Chapter {number}', es: 'Capítulo {number}' }), { ...label,
    fontSize: pt(8.5), letterSpacing: pt(1.7), color: col('accent') },
  at('container', 'top-left', 0, 4)),
  text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(28), lineHeight: 1.08,
    color: col('ink'), align: 'left', overflow: 'wrap' }, below('kicker', 3, { width: 'fill' })),
  byline,
  { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.2), color: col('accent'),
    placement: { ...below('byline', 6), size: { width: mm(16) } } },
] } };
// #endregion

// #region running-heads: contributor on the verso, short title on the recto, folios outside
function head(id, content, parity, placement, look = {}) { // hoisted: the byline uses it
  return { ...text(id, content, { ...label, fontWeight: 500, fontSize: pt(7.5),
    letterSpacing: pt(1.1), color: col('muted'), ...look }, placement), parity, pages: 'body' };
}
const folio = { fontWeight: 600, color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, 13), folio),
  versoAuthor,
  head('recto-title', '{attr.short}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), 13),
    { align: 'right' }),
  head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, 13),
    { ...folio, align: 'right' }),
] };
const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all',
  at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] };
// #endregion

// #region front: the title page on a red field, and the contents
const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
const cover = { enabled: true, slot: { elements: [
  { kind: 'box', id: 'field', style: { backgroundColor: col('accent') },
    placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: mm(150) } } },
  text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(58), lineHeight: 1,
    color: col('paper'), align: 'left', overflow: 'wrap' },
  { ...at('page', 'top-left', MARGIN.inner, 64), size: { width: mm(118) } }),
  text('subtitle', '{subtitle}', { fontFamily: TEXT, italic: true, fontSize: pt(15),
    color: col('paper'), align: 'left' }, below('title', 5)),
  text('editors', '{attr.editors}', { ...label, fontSize: pt(9), letterSpacing: pt(1.8),
    color: col('accent') }, at('page', 'top-left', MARGIN.inner, 162)),
  text('contributors', '{attr.contributors}', { fontFamily: TEXT, italic: true, fontSize: pt(12),
    color: col('ink'), align: 'left' }, below('editors', 3)),
  text('imprint', '{attr.imprint}', { ...label, fontWeight: 500, fontSize: pt(7.5),
    letterSpacing: pt(1.5), color: col('muted') }, at('page', 'bottom-left', MARGIN.inner, -16)),
] } };
const contentsOpener = { enabled: true, minHeight: mm(40), slot: { elements: [
  text('kicker', '{title}', { ...label, fontSize: pt(8.5), letterSpacing: pt(1.7),
    color: col('accent') }, at('container', 'top-left', 0, 4)),
  text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(28), color: col('ink'),
    align: 'left' }, below('kicker', 3)),
] } };
const contents = {
  levels: [{ level: 1, fontFamily: TEXT, fontSize: pt(12.5), lineHeight: pt(LEAD),
    numberFontFamily: LABEL, numberFontSize: pt(10), numberFontWeight: 600,
    numberColor: col('accent'), numberWidth: mm(6), numberGap: mm(3), marginBottom: pt(LEAD) }],
  pageNumber: { fontFamily: LABEL, fontSize: pt(9), fontWeight: 600, color: col('ink'),
    width: mm(7) },
  leader: { char: '. ', gap: mm(2) },
  subtitle: tocAuthor,
};
// #endregion

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: t({ en: 'en-us', es: 'es' }),
  colorPalette,
  citations,
  page: { sizePreset: 'custom', width: mm(155), height: mm(235), 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' },
  bodyText: { fontFamily: TEXT, fontSize: pt(9.6), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false,
    hyphenation: { enabled: true }, optimalLineBreaking: true, maxWordSpacing: 1.7,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true },
  headings: { fontFamily: DISPLAY, fontWeight: 400, color: col('ink'),
    // A page that ends short may open at most one extra line above a subhead.
    balancing: { maxLinesPerHeading: 1 }, levels: [
    // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
    // 'any': an essay opens on the next page, recto or verso, as in most edited volumes.
    { level: 1, numberingTemplate: '{1}', marginBottom: pt(0),
      breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener },
    { level: 2, fontFamily: TEXT, italic: true, fontSize: pt(11), lineHeight: pt(LEAD * 1.5),
      numberingTemplate: '', marginTop: pt(LEAD * 0.5), marginBottom: pt(0) },
  ] },
  headingStyles: [
    { id: 'cover', ...bare, span: 'page', advancedDesign: cover },
    { id: 'contents', ...bare, advancedDesign: contentsOpener },
    // The references heading: the label face in red, out of the contents.
    { id: 'references', toc: false, fontFamily: LABEL, fontWeight: 600, italic: false,
      fontSize: pt(8.5),
      letterSpacing: pt(1.7), textTransform: 'uppercase', color: col('accent'),
      lineHeight: pt(LEAD * 2), marginTop: pt(LEAD), marginBottom: pt(0) },
  ],
  toc: contents,
  calloutStyles: [{ id: 'colophon', placement: 'bottom', backgroundEnabled: false,
    stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') },
    padding: { top: mm(2.5), right: pt(0), bottom: pt(0), left: pt(0) }, marginBottom: pt(0),
    body: { fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(10.5), color: col('muted'),
      textAlign: 'left', firstLineIndent: pt(0) } }],
  header, footer,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const front = String.raw`---
title: "La página en obra"
subtitle: "Ensayos sobre imprenta, tipos y lectura"
---

# La página en obra {style="cover" editors="Edición de Helen Ashby y Tomás Ribera" contributors="Margaret Lowe · Daniel Okafor · Lucía Ferrer" imprint="Quire Editions · Edimburgo · 2026"}

# Índice {style="contents"}

:::toc

:::callout{type="colophon"}
Compuesto en Libre Caslon Text, Libre Caslon Display e IBM Plex Sans Condensed (SIL OFL). Texto: original, CC BY 4.0. La obra, sus editores y sus autores son ficticios; las obras que citan son reales.
:::
`; // title page and contents (content.<lang>.md)
const lowe = String.raw`# La jornada del cajista {author="Margaret Lowe" short="La jornada del cajista"}

Toda página impresa a mano pasaba por las manos de un cajista antes de llegar a la prensa. El cajista trabajaba de pie ante un par de cajas, las mayúsculas en la de arriba y las minúsculas en la de abajo; leía unas pocas palabras del original y sacaba los tipos uno a uno para ir colocándolos en el componedor que sostenía con la mano izquierda. Cuando el componedor estaba lleno, pasaba las líneas a la galera; cuando había líneas suficientes para una página, la ataba con un cordel y la apartaba hasta que pudiera imponerse la forma entera y acuñarla en su rama. Joseph Moxon describió esta rutina en sus *Mechanick Exercises* de 1683-1684, el primer manual completo de imprenta en inglés, y casi todo lo que los bibliógrafos escribieron después sobre el cajista parte de su relato [@gaskell1972].

El trabajo se pagaba a destajo. Al cajista se le contaban los cuadratines que componía, y su rapidez, su ortografía y su cuidado decidían lo que recibía el lector. Por eso importa el cajista a quien edita hoy un texto antiguo: entre el manuscrito del autor y el pliego impreso había un hombre con sus propias costumbres, que trabajaba contra el reloj y podía leer mal una palabra, quedarse sin una letra o cambiar una grafía para que la línea cuadrase.

## Lo que cuentan los tipos

Los propios tipos guardan memoria de ese trabajo. Una letra con el remate mellado o el asta rota imprime el mismo defecto allí donde se use, y como el cajista devolvía los tipos a la caja cuando la página ya estaba tirada, la letra dañada reaparece unas páginas más adelante. Charlton Hinman siguió cientos de esas letras a lo largo del Primer Folio de Shakespeare, de 1623, y reconstruyó el orden en que se compusieron sus páginas, qué cajista compuso cada una y cómo pasaba el trabajo de una caja a otra [@hinman1963]. Su método exigía paciencia y una máquina de cotejo que lleva su nombre, pero partía de una idea sencilla: los tipos de un taller son finitos y sus movimientos dejan rastro.

La ortografía ofrecía una segunda clase de indicios. Un cajista prefería *doe* y *goe*; otro, *do* y *go*; uno ponía dos puntos donde su compañero ponía una coma. Allí donde el original no imponía una forma, asomaba la preferencia del cajista, y una página compuesta por uno se lee distinta de la compuesta por otro. Los bibliógrafos reunieron esas preferencias en perfiles y los usaron para atribuir páginas y para pesar la autoridad de una lectura [@gaskell1972].

## Impresores imaginarios

El punto débil de estos métodos estaba en lo que suponían del taller. Los análisis de recurrencia de tipos solían dar por hecho que un taller trabajaba en un libro cada vez, con el cajista componiendo las páginas en orden y la prensa tirando cada forma en cuanto estaba lista. D. F. McKenzie contrastó esa imagen con los registros de la imprenta de la Universidad de Cambridge entre 1696 y 1712, que se conservan con un detalle poco común, y comprobó que no se sostenía [@mckenzie1969]. Había varios libros en curso a la vez. Los cajistas pasaban de un trabajo a otro según llegaba el original; los prensistas tiraban en la misma semana pliegos de libros distintos; el trabajo se paraba y se reanudaba según lo permitían el papel, el dinero o las correcciones del autor.

McKenzie concluyó que los bibliógrafos habían construido sus argumentos sobre «impresores de la mente», un taller ordenado que existía en sus razonamientos y no en el archivo. Una pauta hallada en los tipos podía explicarse con más de una secuencia de trabajo, y no cabía deducir de ella una sola sin pruebas externas. Su artículo no acabó con el estudio de los cajistas, pero le cambió el tono: desde entonces, cada inferencia declara las condiciones en que vale.

## Lo que hacía el cajista

El cajista producía además algo mayor que una página. Como una misma composición imprimía todos los ejemplares de un pliego, una edición de varios cientos de ejemplares llevaba el mismo texto, página a página, a lectores que nunca se conocerían. Elizabeth Eisenstein hizo de esa repetición el centro de su relato sobre la Europa moderna: el ejemplar idéntico, sostenía, permitió a estudiosos de ciudades distintas comparar las mismas tablas, los mismos mapas y los mismos textos, corregirlos en ediciones posteriores y apoyarse en el trabajo de los demás [@eisenstein1979]. El segundo ensayo de este volumen vuelve sobre su tesis y sobre las objeciones que encontró.

Y, sin embargo, los ejemplares de una edición nunca eran del todo idénticos. Muchas erratas se descubrían con el pliego ya en la prensa; se aflojaba la forma, se corregía y no se tiraban los pliegos ya impresos, de modo que estados corregidos y sin corregir acababan encuadernados en la misma edición [@gaskell1972]. Hinman encontró esas variantes de prensa por todo el Folio [@hinman1963]. La jornada del cajista producía, pues, la estabilidad que describió Eisenstein y también las pequeñas inestabilidades que los bibliógrafos se pasan la vida cotejando.

## Referencias {style="references"}

:::bibliography

:::references{format=bibtex}
@book{gaskell1972,
  author = {Gaskell, Philip}, title = {A New Introduction to Bibliography},
  publisher = {Clarendon Press}, address = {Oxford}, year = 1972}
@book{hinman1963,
  author = {Hinman, Charlton},
  title = {The Printing and Proof-Reading of the First Folio of Shakespeare},
  publisher = {Clarendon Press}, address = {Oxford}, year = 1963}
@article{mckenzie1969,
  author = {McKenzie, D. F.},
  title = {Printers of the Mind: Some Notes on Bibliographical Theories and Printing-House Practices},
  journal = {Studies in Bibliography}, volume = 22, pages = {1--75}, year = 1969}
@book{eisenstein1979,
  author = {Eisenstein, Elizabeth L.},
  title = {The Printing Press as an Agent of Change: Communications and Cultural Transformations in Early-Modern Europe},
  publisher = {Cambridge University Press}, address = {Cambridge}, year = 1979}
:::
`; // content.lowe.<lang>.md: chapter 1, and so on
const okafor = String.raw`# Lo que significaba la fijeza {author="Daniel Okafor" short="Lo que significaba la fijeza"}

Pocos libros de historia del libro han dado tanto que discutir como el de Elizabeth Eisenstein. *The Printing Press as an Agent of Change* apareció en dos volúmenes en 1979 y sostenía que el paso del manuscrito a la imprenta fue por sí mismo una causa del renacer del saber en el Renacimiento, de la Reforma y de la nueva ciencia del siglo XVII [@eisenstein1979]. La tesis descansaba en unos pocos rasgos que la imprenta tenía y el manuscrito no: la difusión de cientos de ejemplares a la vez, la normalización del texto y de la imagen y, sobre todo, lo que ella llamó fijeza tipográfica, la capacidad de una edición para conservar un texto sin cambios y permitir que se corrigiera y mejorara en la siguiente.

La tesis era ambiciosa, y su recepción se dividió según una línea conocida. Los historiadores de las ideas encontraron en ella una explicación de por qué el saber se volvió acumulativo después de 1500. Los historiadores de la imprenta señalaron la variedad de lo que salía de las prensas, desde almanaques plagados de erratas hasta ediciones piratas, y preguntaron si la fijeza era una propiedad de lo impreso o un resultado que algunos impresores lograban algunas veces.

## Crédito y piratería

Adrian Johns dio a la segunda respuesta su forma más completa [@johns1998]. Al estudiar el comercio del libro en el Londres del siglo XVII, comprobó que los lectores tenían buenos motivos para desconfiar de lo que compraban. Las ediciones se reimprimían sin permiso, se atribuían a autores que no las habían escrito o salían con la fecha y el pie de imprenta equivocados. La Stationers' Company llevaba un registro de títulos para resolver las disputas entre sus miembros, pero ese registro protegía los acuerdos del gremio, no el texto del autor. Quien quería saber si un libro impreso era lo que decía ser dependía de la reputación del impresor y del librero, de quienes respondían por la edición y de los usos de la tienda donde se vendía.

Para Johns, por tanto, la fijeza era algo que había que fabricar. Exigía el trabajo de autores que vigilaban sus pruebas, de libreros que cuidaban su nombre y de instituciones como la Royal Society, que autorizaba y supervisaba los libros que patrocinaba. Su ejemplo más citado es el del astrónomo real John Flamsteed, que vio impresas en 1712 sus observaciones en una edición que no había aprobado y pasó años intentando recuperarla y suprimirla. Donde Eisenstein veía un rasgo de la técnica, Johns veía un conjunto de prácticas que había que aprender y que podían fallar.

## Circuitos y modelos

El debate entre ambas posturas se sigue mejor con un mapa de lo que atraviesa un libro. Robert Darnton trazó uno en 1982: un circuito de comunicación que va del autor al editor, a los impresores, a los transportistas, a los libreros y a los lectores, y de nuevo al autor, con cada etapa marcada por condiciones económicas, políticas e intelectuales [@darnton1982]. El modelo situó la historia del libro entre las personas que lo manejaban, y muchos estudios de los años ochenta lo recorrieron etapa por etapa.

Thomas Adams y Nicolas Barker objetaron que el circuito de Darnton describía el comercio y perdía de vista el objeto. Propusieron un modelo centrado en el propio libro y en los momentos de su vida: la publicación, la fabricación, la distribución, la recepción y la supervivencia [@adams1993]. La diferencia pesa en la discusión sobre la fijeza. En el circuito de Darnton, la fijeza pertenece a las relaciones entre impresores, vendedores y lectores; en el modelo de Adams y Barker es también cuestión de qué ejemplares sobrevivieron, dónde y en qué estado.

## La forma del texto

D. F. McKenzie amplió todavía más la pregunta en sus Panizzi Lectures, publicadas en 1986 [@mckenzie1986]. La bibliografía, sostuvo, debía estudiar los textos como formas registradas y los procesos de su transmisión, producción y recepción incluidas, y tratar la forma de un texto como parte de su sentido. Su ejemplo fue William Congreve, cuyas comedias cambiaron de carácter al recomponerse para las *Works* reunidas de 1710: la nueva disposición dividía las escenas de otro modo, marcaba las entradas de otra manera e invitaba a otra clase de lectura.

Desde la posición de McKenzie, la disputa sobre la fijeza se parece menos a una disputa. Una edición puede fijar las palabras de un texto y cambiar a la vez su manera de leerse; una reimpresión puede conservar el sentido y perder la forma que lo sostenía. Eisenstein describió lo que la imprenta hacía posible, y Johns, lo que costaba que esa posibilidad se cumpliera [@eisenstein1979; @johns1998]. Las dos son historias de las mismas páginas, escritas desde extremos opuestos del circuito.

## Referencias {style="references"}

:::bibliography

:::references{format=bibtex}
@book{eisenstein1979,
  author = {Eisenstein, Elizabeth L.},
  title = {The Printing Press as an Agent of Change: Communications and Cultural Transformations in Early-Modern Europe},
  publisher = {Cambridge University Press}, address = {Cambridge}, year = 1979}
@book{johns1998,
  author = {Johns, Adrian}, title = {The Nature of the Book: Print and Knowledge in the Making},
  publisher = {University of Chicago Press}, address = {Chicago}, year = 1998}
@article{darnton1982,
  author = {Darnton, Robert}, title = {What Is the History of Books?},
  journal = {Daedalus}, volume = 111, number = 3, pages = {65--83}, year = 1982}
@incollection{adams1993,
  author = {Adams, Thomas R. and Barker, Nicolas},
  title = {A New Model for the Study of the Book},
  booktitle = {A Potencie of Life: Books in Society}, editor = {Barker, Nicolas},
  publisher = {British Library}, address = {London}, pages = {5--43}, year = 1993}
@book{mckenzie1986,
  author = {McKenzie, D. F.}, title = {Bibliography and the Sociology of Texts},
  series = {The Panizzi Lectures}, publisher = {British Library}, address = {London},
  year = 1986}
:::
`;
const ferrer = String.raw`# Punzones, matrices y el nombre de Garamond {author="Lucía Ferrer" short="Punzones y matrices"}

Antes de que un cajista pudiera componer una página, alguien tenía que fabricar las letras que sacaba de la caja. Lucien Febvre y Henri-Jean Martin calcularon que antes de 1500 se habían impreso en Europa entre quince y veinte millones de ejemplares de libros [@febvre1976]. Cada tipo de letra empezó como una serie de punzones de acero tallados a mano al tamaño que vería el lector.

La secuencia de trabajo está bien documentada, aunque la destreza manual que exigía se ha perdido casi del todo [@carter1969]. El grabador de punzones daba forma al extremo de una barra de acero hasta obtener la letra invertida; la limaba, la grababa y la comprobaba con pruebas de humo. Los blancos interiores de la letra, los ojos de la *a*, la *e* o la *o*, solían abrirse en el punzón con un contrapunzón, lo que permitía repetir el mismo ojo en varias letras de la fundición. Fred Smeijers, que ha tallado punzones con sus manos, sostiene que el contrapunzón explica buena parte de la coherencia de las letras del siglo XVI: las formas se parecían porque se hacían con las mismas herramientas [@smeijers1996].

## Del punzón al tipo

El punzón templado se hundía en una barra de cobre para obtener una matriz. Una vez justificada, se encajaba en un molde manual, una caja ajustable de acero y madera en la que el fundidor vertía una aleación de plomo, estaño y antimonio. Cada colada daba un tipo, y la fundición terminada, frotada y repasada, se vendía a los impresores al peso [@gaskell1972].

Los punzones eran el capital del fundidor: se prestaban, se heredaban o se vendían, y una serie de matrices batidas con ellos permitía a otra fundición fundir la misma letra. Por eso las letras de unos pocos grabadores franceses de mediados del siglo XVI se extendieron por Europa en una generación.

## De quién son las letras

Eso mismo complicaba las atribuciones. Una letra aparece en el libro de un impresor mucho después de haberse tallado, con el nombre del impresor y no con el del grabador. Harry Carter ordenó las pruebas disponibles: muestrarios, inventarios, contratos y los propios libros [@carter1969]. Hendrik Vervliet las aplicó durante décadas al Renacimiento francés, y sus catálogos devolvieron a otros grabadores de su tiempo, entre ellos Robert Granjon y Pierre Haultin, muchos tipos que se habían atribuido a Claude Garamont [@vervliet2008].

El nombre de Garamond tuvo además una historia moderna propia. Cuando las fundiciones de principios del siglo XX recuperaron la «Garamond» para la composición mecánica, varias tomaron como modelo unos tipos que no había tallado Garamont, sino Jean Jannon, impresor en Sedán, unos sesenta años después de la muerte de Garamont. Beatrice Warde lo demostró en 1926. El nombre se quedó.

## Lo que ve el lector

Nada de esto se ve en la página tal como la encuentra el lector. El lector juzga un libro por lo bien que se lee. El bibliógrafo ve, en esa misma página, la huella de las limas de un grabador, el desgaste de una matriz y las costumbres del fundidor que la justificó. Los impresores antiguos compraban sus tipos por cuerpos con nombre propio, como la lectura o el entredós, y los juzgaban igual que el lector, por la página que hacían [@gaskell1972]. La historia de los tipos empieza en el acero, pero siempre se escribió para esa página.

## Referencias {style="references"}

:::bibliography

:::references{format=bibtex}
@book{febvre1976,
  author = {Febvre, Lucien and Martin, Henri-Jean},
  title = {The Coming of the Book: The Impact of Printing, 1450--1800},
  translator = {Gerard, David}, publisher = {NLB}, address = {London}, year = 1976}
@book{carter1969,
  author = {Carter, Harry}, title = {A View of Early Typography up to about 1600},
  publisher = {Clarendon Press}, address = {Oxford}, year = 1969}
@book{smeijers1996,
  author = {Smeijers, Fred},
  title = {Counterpunch: Making Type in the Sixteenth Century, Designing Typefaces Now},
  publisher = {Hyphen Press}, address = {London}, year = 1996}
@book{gaskell1972,
  author = {Gaskell, Philip}, title = {A New Introduction to Bibliography},
  publisher = {Clarendon Press}, address = {Oxford}, year = 1972}
@book{vervliet2008,
  author = {Vervliet, Hendrik D. L.},
  title = {The Palaeotypography of the French Renaissance: Selected Papers on Sixteenth-Century Typefaces},
  publisher = {Brill}, address = {Leiden}, year = 2008}
:::
`;
// #region chapters: one document per contributor, each with its own references
// A chapter's :::bibliography lists what that document cites; one Markdown file holding all
// three essays would make every list repeat the works of the chapters before it.
const chapters = [front, lowe, okafor, ferrer].map((markdown) => ({ markdown }));
const book = () => buildBundle({ chapters, config: config(), resources: [] });
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // text, display and label faces (gotcha: fonts-first)
  'Libre Caslon Text': ['400', '400i', '700'],
  'Libre Caslon Display': ['400'],
  'IBM Plex Sans Condensed': ['400', '500', '600'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const all = chapters.map((chapter) => chapter.markdown).join('\n');
await loadFonts(FONTS, all);
const docs = await buildWithFonts(book, all); // one VDTDocument per Markdown document
showPages(docs, { title: t({ en: 'The Working Page', es: 'La página en obra' }) });
offerPdf(() => renderToPdf(docs, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);

// ─── 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 v1 ── the same in every recipe · postext.dev/cookbook ────────
// Postext measures text with the faces the browser has loaded, and caches the
// widths, so every face must be ready before the first build. Faces come from
// Fontsource: the same static files the PDF embeds, so screen and PDF agree.

/** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = {
    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',
  };
  const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin'];
  const jobs = [];
  let added = 0;
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const meta = optional ? await fontsourceMeta(family) : null;
    for (const spec of new Set(specs)) {
      const weight = parseInt(spec, 10);
      const style = spec.endsWith('i') ? 'italic' : 'normal';
      if (hasFace(family, weight, style)) continue;
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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().then((ready) => { document.fonts.add(ready); added++; }, () => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return added;
}

/** Runs `build` (a buildDocument or buildBundle call) and checks the faces
 *  the pages use. A regular face missing from FONTS is loaded with a warning;
 *  bold and italic variants are loaded when the family ships them. Then the
 *  measurement caches are cleared and the build runs again. */
async function buildWithFonts(build, text = '') {
  const tried = new Set();
  for (let round = 0; round < 3; round++) {
    kitStatus('Laying out…');
    await new Promise(requestAnimationFrame);          // let the status paint first
    const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; });
    const wanted = { base: {}, variants: {} };
    for (const { font, base } of [result].flat().flatMap(fontStringsOf)) {
      const { family, weight, style } = parseFont(font);
      const key = `${family}|${weight}|${style}`;
      if (tried.has(key) || hasFace(family, weight, style)) continue;
      tried.add(key);
      (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`);
    }
    if (Object.keys(wanted.base).length) {
      console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`);
    }
    const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true });
    if (added === 0) return result;
    clearMeasurementCache();
  }
  throw new Error('The fonts did not settle after three builds.');
}

/** Every font string of the layout. `base` marks a block's own face; its
 *  bold, italic and bold-italic variants are listed whether or not used. */
function fontStringsOf(doc) {
  const found = new Map();
  const walk = (node) => {
    if (!node || typeof node !== 'object') return;
    if (Array.isArray(node)) { node.forEach(walk); return; }
    for (const [key, value] of Object.entries(node)) {
      if (typeof value === 'string' && /fontString$/i.test(key)) {
        found.set(value, found.get(value) || key === 'fontString');
      } else if (value && typeof value === 'object') walk(value);
    }
  };
  walk(doc.pages);
  walk(doc.blocks);
  return [...found].map(([font, base]) => ({ font, base }));
}

/** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }.
 *  A string with no weight ('95.8px Young Serif', from a design text) is 400. */
function parseFont(font) {
  const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim());
  if (!m) throw new Error(`Unexpected font string: ${font}`);
  const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]);
  return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' };
}

/** True when a loaded FontFace covers exactly this family, weight and style
 *  (document.fonts.check() is also true 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;
}

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

/** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), 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 v1 ── the same in every recipe · postext.dev/cookbook ───────
/** Shows the pages as facing spreads on a dark desk: the first page is a
 *  recto on its own, then verso | recto pairs, as in a bound book. Pages
 *  are painted when they scroll near the screen. */
function showPages(docs, { title, width = 460 } = {}) {
  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, { 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 · pdf v1 ── the same in every recipe that exports a PDF ──────────────
/** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen
 *  used, snapping to a weight the family ships and falling back to upright
 *  when it has no italic: the PDF asks for every face a block could use. */
async function fontsourceProvider(family, weight, style) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  const weights = meta?.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style;
  const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`);
  if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`);
  return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
}

/** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new
 *  tab, since CodePen's preview frame cannot show PDFs) and a download link. */
function offerPdf(makePdf, filename) {
  viewer();
  const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' });
  button.dataset.postextPdf = filename;
  button.addEventListener('click', async () => {
    button.disabled = true;
    button.textContent = 'Building the PDF…';
    try {
      const bytes = await makePdf();
      const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
      const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`;
      button.replaceWith(
        Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }),
        Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` }));
    } catch (error) {
      button.disabled = false;
      button.textContent = 'Build the PDF';
      kitFail(error);
    }
  });
  document.getElementById('pt-actions').append(button);
}

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

## Variants

### Una sola llista per a tot el volum

Algunes editorials reuneixen totes les obres en una bibliografia final. Canvia l'abast i treu els tres títols `## Referencias`; la llista passa a anar darrere de l'últim capítol.

```diff
-    scope: 'chapter',
-    title: '', // no bold title paragraph: each chapter has its own ## References heading
+    scope: 'book',
+    title: 'Bibliografía',
```

### Cita en APA

Un volum de ciències socials canvia l'estil i res més.

```diff
-  style: 'chicago-author-date', // the humanities' author-date: (Eisenstein 1979)
+  style: 'apa',
```

## Errors freqüents

- **Una clau definida dues vegades conserva la primera entrada, sense avís.** Les referències valen per a tot el llibre: una clau escrita en dos capítols conserva la primera definició i la segona s'ignora sense avís. En un llibre de diversos autors, posa un prefix a les claus de cadascun.
- **Qualsevol objecte headings desactiva el salt de pàgina de l'H1.** Per defecte un H1 salta a una pàgina senar (always-odd), però qualsevol objecte headings anul·la aquest valor, de manera que els capítols van seguits i span: 'page' no fa res. Torna a declarar headings.levels[0].breakBefore: { enabled: true, parity } a cada configuració.
- **buildBundle no passa metadades d'un capítol a un altre; {totalPages} és per capítol.** buildBundle no passa metadades d'un capítol al següent: {title}, {author} i els altres surten només del frontmatter del mateix capítol, així que tot capítol sense frontmatter els deixa buits (i el Sandbox descarta el frontmatter de tots els capítols tret del primer), i {totalPages} compta les pàgines de cada capítol. Escriu el títol del llibre literalment a les capçaleres.
- **Valors d'atribut: sense { ni }; cometes simples si porten ".** Un valor d'atribut acaba a la clau de tancament, així que no pot contenir { ni }. Un valor que porti cometes dobles va entre cometes simples; el signe de dòlar no dona problemes.
- **Un estil de títol hereta el salt de pàgina del seu nivell.** Una entrada de headingStyles pren del seu nivell de títol tot allò que no fixa, també breakBefore. Un índex o un colofó amb estil sobre un H1 després d'un :::pagebreak hereta la paritat 'odd' i cau darrere d'una pàgina en blanc. Dona a aquest estil breakBefore: { enabled: false }.
- **Carrega totes les fonts abans de compondre.** La composició mesura el text amb les fonts que el navegador ha carregat i en desa les amplades, així que una font que arriba després de la primera composició deixa talls de línia erronis i un PDF que ja no coincideix amb la pantalla. Carrega abans tots els pesos i estils, i crida clearMeasurementCache() abans de recompondre si alguna arriba tard.
- **Posa entre cometes cada valor del frontmatter.** YAML llegeix title: 1984 com un nombre i una data com un objecte Date, i els valors que no són cadenes s'imprimeixen buits als marcadors i deixen el PDF sense títol. Posa entre cometes cada valor: title: "1984".
- **Una configuració es desa a la memòria cau per identitat: crea un objecte nou.** El motor desa a la memòria cau les configuracions resoltes segons la identitat de l'objecte, de manera que modificar el mateix objecte i tornar a compondre reutilitza el resultat anterior. Crea un objecte nou a cada composició: per això la configuració d'una recepta és una funció, config().

- `scope: 'chapter'` llista el que el document cita abans del `:::bibliography`, no el que cita la seva secció H1. Amb tots els assajos en un mateix fitxer Markdown, la segona llista repeteix les obres del primer assaig i la tercera, les dels dos anteriors.
- A tot el llibre mana la primera definició de cada clau. Si dos autors fan servir `smith2001` per a obres diferents, l'assaig posterior cita sense avís el llibre del primer: posa un prefix a les claus de cada autor o confronta les llistes amb els originals.
- La desambiguació es calcula per al llibre sencer. Dues obres diferents de Smith del 2001 en capítols diferents poden sortir com a 2001a i 2001b, encara que la llista d'un capítol només en contingui una.

## Crèdits

- Recepta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Tipus de lletra: Libre Caslon Text (OFL-1.1), Libre Caslon Display (OFL-1.1), IBM Plex Sans Condensed (OFL-1.1)
- Codi: MIT · Contingut d'exemple: CC-BY-4.0

## Relacionades

- [Núm. 087 · Un capítol de tesi citat en APA 7](https://postext.dev/ca/cookbook/apa-thesis-with-bibtex.md): Un capítol de tesi doctoral amb citacions [@clau, p. 33] que surten en APA 7 gràcies a citeproc-js, amb la llista de referències presa d'un bloc BibTeX. · Nivell 2 (Intermedi) · Articles i treballs acadèmics, Informes i memòries
- [Núm. 030 · Antologia amb signatures d'autor](https://postext.dev/ca/cookbook/anthology-with-bylines.md): Tres assaigs sobre caminar amb títols que porten autor, any i revista: l'obertura imprimeix les tres dades; les capçaleres parelles i l'índex, només l'autor. · Nivell 2 (Intermedi) · Narrativa, teatre i prosa literària
- [Núm. 007 · Un llibre fet de capítols solts](https://postext.dev/ca/cookbook/book-from-chapters.md): buildBundle compon cinc fitxers Markdown com un sol llibre: cada capítol obre en pàgina senar i pàgines, capítols i figures es numeren correlativament. · Nivell 3 (Avançat) · Manuals, guies i obres de consulta
