# 章ごとに参考文献を持つ編著

> 3人の寄稿者による3編の論考。buildBundleでそれぞれを独立した文書にし、各章はその章が引用した文献だけを並べたChicago著者・年方式の一覧で終わります。

- HTML版: https://postext.dev/ja/cookbook/edited-volume-chapter-bibliographies
- レシピ No. 098 · 本の構成 · 難易度 3 (上級) · 出力: Canvas, PDF
- ジャンル: 論文・学術書, 教科書
- 必要なもの postext ≥ 1.12.1, postext-pdf ≥ 1.12.1 · テスト環境 1.12.1, postext-pdf 1.12.1 ／テスト日 2026-10-01
- ページ: [1](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p01.webp?v=213364cd), [2](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p02.webp?v=213364cd), [3](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p03.webp?v=213364cd), [4](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p04.webp?v=213364cd), [5](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p05.webp?v=213364cd), [6](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p06.webp?v=213364cd), [7](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p07.webp?v=213364cd), [8](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p08.webp?v=213364cd), [9](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p09.webp?v=213364cd), [10](https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/p10.webp?v=213364cd)
- PDF: https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/edited-volume-chapter-bibliographies.pdf?v=213364cd
- Sandboxで開く: https://postext.dev/ja/sandbox#recipe=edited-volume-chapter-bibliographies&lang=en (.postext: https://postext.dev/cookbook/edited-volume-chapter-bibliographies/en/edited-volume-chapter-bibliographies.postext)
- 最終更新: 2026-10-01
- 他の言語: [en](https://postext.dev/en/cookbook/edited-volume-chapter-bibliographies.md), [es](https://postext.dev/es/cookbook/edited-volume-chapter-bibliographies.md), [ca](https://postext.dev/ca/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)

## かんたんな説明

異なる著者による論考を集めた本です。各論考の題の下に著者名が入り、論文集の通例どおり、最後にそれぞれの文献一覧が付きます。

## できあがり

『The Working Page』は印刷の歴史をめぐる小さな論文集で、2人の研究者が編集し、3人が執筆しています。各論考は寄稿者から、それぞれ自分のBibTeXブロックを持つMarkdownファイルとして届き、本もその形のまま保ちます。判型は155 × 235mm、書体はLibre Caslon、扉は赤、章は番号、題、寄稿者名で始まります。各章はChicago著者・年方式による自分の「References」一覧で終わり、そこにはその論考が引用した文献だけが入ります。Eisensteinの『Printing Press as an Agent of Change』は2人の寄稿者が引用しているので、両方の一覧に載ります。Gaskellの『New Introduction to Bibliography』も同じく、第1章と第3章に載ります。

**このレシピが答える問い:**

- 編著の各章に、それぞれの著者名と文献一覧を付けるには？
- APA、IEEEなどの引用スタイルで、文献を引用し参考文献を作るには？

## 手短な答え

```js
// script.js, 行 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.
```

## 材料

**学べること**

- [参考文献から作る文献一覧](https://postext.dev/ja/docs/document-format.md#引用と参考文献): 文書の参考文献（フロントマターまたはBibTeXブロック）から作る引用文献の一覧です。:::bibliographyを置いた位置か、最後の章の後に入ります。
- [引用スタイルによる引用](https://postext.dev/ja/docs/document-format.md#引用と参考文献): 引用は[@key, p. 33]のように書き、設定で選んだCSLスタイル（APA、Chicago、MLA、IEEE、Vancouver、ISO 690、GB/T 7714…）で整形され、文献一覧の項目にリンクされます。

**ほかに使うもの**

- [章ごとに組む本](https://postext.dev/ja/docs/configuration.md#バンドルを組んで描画する)
- [見出しの属性](https://postext.dev/ja/docs/document-format.md#見出しの属性)
- [デザインした章扉](https://postext.dev/ja/docs/configuration.md#幅と詳細デザイン)
- [目次](https://postext.dev/ja/docs/configuration.md#目次)
- [柱とノンブル](https://postext.dev/ja/docs/configuration.md#柱とノンブル)
- [ページの役割ごとの柱](https://postext.dev/ja/docs/configuration.md#テキスト要素)
- [見出しスタイル](https://postext.dev/ja/docs/configuration.md#見出しスタイル)
- [番号のない章](https://postext.dev/ja/docs/configuration.md#見出しスタイル)
- [表紙、扉、奥付](https://postext.dev/ja/docs/configuration.md#見出しスタイル)
- [デザイン要素の基準位置](https://postext.dev/ja/docs/configuration.md#要素の配置)
- [フロートする囲み](https://postext.dev/ja/docs/configuration.md#calloutコンテナー)
- [見開きの余白](https://postext.dev/ja/docs/configuration.md#見開きの余白)
- [セマンティックカラーパレット](https://postext.dev/ja/docs/configuration.md#カラーパレット)
- [PDFの書き出し](https://postext.dev/ja/docs/configuration.md#pdfの生成)
- [囲み](https://postext.dev/ja/docs/configuration.md#囲みスタイル)
- [段末そろえ](https://postext.dev/ja/docs/configuration.md#段末そろえ)
- [番号付き見出し](https://postext.dev/ja/docs/configuration.md#レベルごとの上書き)
- [用紙の色](https://postext.dev/ja/docs/configuration.md#ページ)
- [PDFに埋め込むフォント](https://postext.dev/ja/docs/configuration.md#なぜフォントプロバイダーが必要か)
- [節ごとの柱](https://postext.dev/ja/docs/configuration.md#見出しスタイル)

**設定の一覧**

- [`bodyText`](https://postext.dev/ja/docs/configuration.md#本文), [`calloutStyles`](https://postext.dev/ja/docs/configuration.md#囲みスタイル), [`citations`](https://postext.dev/ja/docs/configuration.md#引用), [`colorPalette`](https://postext.dev/ja/docs/configuration.md#カラーパレット), [`footer`](https://postext.dev/ja/docs/configuration.md#柱とノンブル), [`header`](https://postext.dev/ja/docs/configuration.md#柱とノンブル), [`headingStyles`](https://postext.dev/ja/docs/configuration.md#見出しスタイル), [`headings`](https://postext.dev/ja/docs/configuration.md#見出し), [`layout`](https://postext.dev/ja/docs/configuration.md#レイアウト), [`locale`](https://postext.dev/ja/docs/configuration.md#ハイフネーション), [`page`](https://postext.dev/ja/docs/configuration.md#ページ), [`toc`](https://postext.dev/ja/docs/configuration.md#目次)

**API**

- [`LOCALES`](https://postext.dev/ja/docs/document-format.md#引用と参考文献), [`STYLES`](https://postext.dev/ja/docs/document-format.md#引用と参考文献), [`buildBundle`](https://postext.dev/ja/docs/configuration.md#バンドルを組んで描画する), [`clearMeasurementCache`](https://postext.dev/ja/docs/configuration.md#計測キャッシュ), [`createCiteprocEngine`](https://postext.dev/ja/docs/document-format.md#引用と参考文献), [`decompressWoff2`](https://postext.dev/ja/docs/configuration.md#ブラウザー向けフォントプロバイダーfontsource--woff2), [`registerCitationEngine`](https://postext.dev/ja/docs/document-format.md#引用と参考文献), [`renderPageToCanvas`](https://postext.dev/ja/docs/configuration.md#ページをビットマップに描画する), [`renderToPdf`](https://postext.dev/ja/docs/configuration.md#pdfの生成)

**書体**

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

## 作り方

### 1 · 寄稿者ごとに1つの文書

```js
// script.js, 行 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: [] });
```

[手短な答え](#手短な答え)のスコープは、文書のスコープです。`buildBundle`は4つの文書を1冊の本として組むので、ノンブル、章番号、目次は通して続き、一方で各`:::bibliography`は自分のファイルの引用を集めます。文献データは寄稿者が書いた場所に置いたままです。どの章で定義したキーも本全体で有効なので、共有の項目が1つあれば足りたのですが、章が自己完結していれば、ほかの論考の一覧を壊さずに章を入れ替えたり並べ直したりできます。

### 2 · 著者名は見出しの属性

```js
// script.js, 行 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)
```

各論考の題には`author="…"`と、柱に使う短い題`short="…"`が付いています。章扉は題の下に著者名を印字し、偶数ページの柱はそのページが属する論考の著者を示し、目次は各項目の下に1行で著者名を印字します。本の前付けで寄稿者を二度並べるところはありません。

### 3 · 章扉

```js
// script.js, 行 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) } } },
] } };
```

キッカー、題、著者名は版面の上から順に積み重なり、それぞれ1つ前の要素の下に固定されています。そのため第3章のような2行の題では、著者名と罫もいっしょに下がります。48mmの確保分に2行の題が収まるので、どの章でも本文は同じグリッド行から始まります。

### 4 · 著者が大勢いる本の柱

```js
// script.js, 行 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' }] };
```

編著では偶数ページに寄稿者、奇数ページに章を出します。単著なら書名と章を出すところです。柱は`pages: 'body'`で絞り込んでいるので、章扉には下がりノンブルだけが入ります。

### 5 · 前付け

```js
// script.js, 行 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,
};
```

扉と目次は最初の文書にある番号なしの2つの見出しで、柱も目次の項目も持ちません。そのデザインは`{title}`と`{subtitle}`をフロントマターから、編者と寄稿者を見出し自身の属性から読みます。

## レシピの全体

レシピのフォルダーから合成した1つのファイルで、サンプルのテキストとレシピ集の共通キットを埋め込んであり、自分でページを組み立てます。動かすには、空のページの`<script type="module">`に入れるか、新しいCodePenのpenのJSパネルに貼り付けます（モジュールとして）。esm.shからpostextを読み込むので、インストールもビルドも要りません。

- レシピのフォルダー: 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 = 'en'; // @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: "The Working Page"
subtitle: "Essays on Printing, Type and Reading"
---

# The Working Page {style="cover" editors="Edited by Helen Ashby and Tomás Ribera" contributors="Margaret Lowe · Daniel Okafor · Lucía Ferrer" imprint="Quire Editions · Edinburgh · 2026"}

# Contents {style="contents"}

:::toc

:::callout{type="colophon"}
Set in Libre Caslon Text, Libre Caslon Display and IBM Plex Sans Condensed (SIL OFL). Text: original, CC BY 4.0. The volume, its editors and its contributors are fictional; the works they cite are real.
:::
`; // title page and contents (content.<lang>.md)
const lowe = String.raw`# The Compositor’s Day {author="Margaret Lowe" short="The Compositor’s Day"}

Every page printed by hand passed through a compositor before it reached the press. He stood at a pair of cases, the capitals in the upper one and the small letters in the lower, read a few words of his copy and picked the sorts one at a time into the composing stick in his left hand. When the stick was full he slid the lines onto a galley; when there were lines enough for a page he tied them up with cord and set the page aside until the whole forme could be imposed and locked into its chase. Joseph Moxon described the routine in his *Mechanick Exercises* of 1683–84, the first full manual of printing in English, and most of what bibliographers later wrote about the compositor starts from his account [@gaskell1972].

The work was paid by the piece. A compositor was credited with the number of ens he set, and his speed, his spelling and his care decided what the reader received. That is why the compositor matters to anyone who edits an early printed text: between the author’s manuscript and the printed sheet stood a man with his own habits, working against the clock, who could misread a word, run short of a letter or change the spelling to make a line fit.

## Reading the evidence of the type

The type itself keeps a record of this work. A sort with a nicked serif or a broken stem prints the same flaw wherever it is used, and once the page has been printed the compositor distributes the type back into the case, so the damaged letter turns up again a few pages later. Charlton Hinman followed hundreds of such letters through the Shakespeare First Folio of 1623 and worked out the order in which its pages were set, which compositor set them and how the work moved between the cases [@hinman1963]. His method rested on patience and a Hinman collator, but its premise was simple: type is a finite stock, and its movements can be traced.

Spelling gave a second kind of evidence. One compositor preferred *doe* and *goe*, another *do* and *go*; one set a colon where his partner set a comma. Where the copy did not dictate a form, the compositor’s preference showed through, and a page set by one man reads differently from a page set by another. Bibliographers assembled such preferences into profiles and used them to assign pages and to weigh the authority of a reading [@gaskell1972].

## Printers of the mind

The weakness of these methods lay in what they assumed about the printing house. Analyses of type recurrence often supposed that a shop worked through one book at a time, a compositor setting the pages in order and a press printing each forme as soon as it was ready. D. F. McKenzie tested that picture against the records of Cambridge University Press for the years 1696 to 1712, which survive in unusual detail, and found that it did not hold [@mckenzie1969]. Several books were in production at once. Compositors moved from one job to another as copy arrived; pressmen printed sheets of different books in the same week; work stopped and restarted as paper, money or the author’s revisions allowed.

McKenzie’s conclusion was that bibliographers had built their arguments on “printers of the mind”, an orderly workshop that existed in their reasoning and not in the archive. A pattern found in the type could be explained by more than one sequence of work, and a single sequence could not be inferred from it without outside evidence. His essay did not end the study of compositors, but it changed its tone: inferences now came with the conditions under which they held.

## What the compositor made

The compositor also produced something larger than any single page. Because the same setting of type printed every copy of a sheet, an edition of several hundred copies carried the same text, page for page, to readers who would never meet. Elizabeth Eisenstein made this repetition the centre of her account of early modern Europe: the identical copy, she argued, allowed scholars in different cities to compare the same tables, maps and texts, to correct them in later editions and to build on one another’s work [@eisenstein1979]. The second essay in this volume returns to her argument and to the objections it met.

Yet the copies of an edition were never quite identical. Errors were often caught while a sheet was on the press, and the forme was unlocked and corrected without the sheets already printed being thrown away, so that corrected and uncorrected states were bound up together in the same edition [@gaskell1972]. Hinman found such press variants throughout the Folio [@hinman1963]. The compositor’s day, in other words, made both the stability that Eisenstein described and the small instabilities that bibliographers spend their careers collating.

## References {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`# What Fixity Meant {author="Daniel Okafor" short="What Fixity Meant"}

Few books in the history of the book have been argued with as long as Elizabeth Eisenstein’s. *The Printing Press as an Agent of Change* appeared in two volumes in 1979 and proposed that the shift from script to print was itself a cause of the Renaissance revival of learning, of the Reformation and of the new science of the seventeenth century [@eisenstein1979]. Her case rested on a few features that print had and manuscript did not: dissemination in hundreds of copies at once, standardisation of text and image, and above all what she called typographical fixity, the capacity of an edition to preserve a text unchanged and to let it be corrected and improved in the next.

The thesis was large, and its reception divided along a familiar line. Historians of ideas found in it an explanation of why scholarship became cumulative after 1500. Historians of printing pointed to the variety of what presses actually produced, from error-filled almanacs to pirated editions, and asked whether fixity was a property of print or a result that some printers achieved some of the time.

## Credit and piracy

Adrian Johns gave the second answer its fullest form [@johns1998]. Studying the London book trade of the seventeenth century, he found that readers had good reason to distrust what they bought. Editions were reprinted without permission, attributed to authors who had not written them, or issued with the wrong date and imprint. The Stationers’ Company kept a register of titles to settle disputes among its members, but the register protected the trade’s own arrangements rather than the author’s text. A reader who wanted to know whether a printed book was what it claimed to be had to rely on the reputation of the printer and the bookseller, on the people who vouched for the edition, and on the conventions of the shop where it was sold.

Fixity, in Johns’s account, was therefore something made. It took the work of authors who watched over their proofs, of booksellers who guarded their names, and of institutions such as the Royal Society, which licensed and supervised the books it sponsored. His most cited example is the Astronomer Royal, John Flamsteed, who saw his observations printed in 1712 in an edition he had not approved and spent years trying to recover and suppress it. Where Eisenstein saw a feature of the technology, Johns saw a set of practices that had to be learned, and that could fail.

## Circuits and models

The debate between the two positions is easier to follow with a map of what a book passes through. Robert Darnton drew one in 1982: a communications circuit that runs from the author to the publisher, the printers, the shippers, the booksellers and the readers, and back to the author, with each stage shaped by economic, political and intellectual conditions [@darnton1982]. The model placed the history of books among the people who handled them, and many studies of the 1980s worked through it stage by stage.

Thomas Adams and Nicolas Barker objected that Darnton’s circuit described the trade and lost sight of the object. They proposed a model built around the book itself and the events in its life: publication, manufacture, distribution, reception and survival [@adams1993]. The difference matters to the argument about fixity. In Darnton’s circuit, fixity belongs to the relations between printers, sellers and readers; in Adams and Barker’s model, it is also a question of which copies survived, where, and in what state.

## The form of the text

D. F. McKenzie widened the question further in his Panizzi lectures, published in 1986 [@mckenzie1986]. Bibliography, he argued, should study texts as recorded forms and the processes of their transmission, including their production and their reception, and it should treat the form of a text as part of its meaning. His example was William Congreve, whose plays changed character when they were reset for the collected *Works* of 1710: the new layout divided the scenes differently, marked entrances in a new way and invited a different kind of reading.

From McKenzie’s position the quarrel over fixity looks less like a quarrel. An edition can fix the words of a text and still change how it reads; a reprint can keep the meaning and lose the form that carried it. Eisenstein described what the press made possible, and Johns described what it took to make that possibility hold [@eisenstein1979; @johns1998]. Both are histories of the same pages, written from different ends of the circuit.

## References {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`# Punches, Matrices and the Name Garamond {author="Lucía Ferrer" short="Punches and Matrices"}

Before a compositor could set a page, someone had to make the letters he picked from the case. Printing spread quickly: Lucien Febvre and Henri-Jean Martin estimated that fifteen to twenty million copies of books had been printed in Europe before 1500 [@febvre1976]. Every one of those copies was printed from type cast in a mould, and every typeface began as a set of steel punches cut by hand at the size the reader would see.

The sequence of work is well documented, though the hand skills it required have mostly been lost [@carter1969]. The punchcutter shaped the end of a steel bar into the reversed form of a letter, filing and engraving it and checking it against smoke proofs. The enclosed spaces of the letter, the counters of *a*, *e* or *o*, were usually struck into the punch with a counterpunch, which let him repeat the same counter in several letters of the fount. Fred Smeijers, who has cut punches himself, argues that the counterpunch explains much of the consistency of sixteenth-century faces: the shapes were related because they were made with the same tools [@smeijers1996].

## From punch to sort

The hardened punch was driven into a bar of copper to make a matrix. The matrix had to be justified, filed so that the letter sat at the right depth and position, and then fitted into a hand mould, an adjustable box of steel and wood into which the caster poured an alloy of lead, tin and antimony. Each pour gave one sort, and the finished type, rubbed and dressed, was sold to printers by weight [@gaskell1972].

Punches were the founder’s capital. They could be used for decades, lent, inherited or sold, and a set of strikes from them allowed another foundry to cast the same face. This is why the faces of a few French punchcutters of the mid-sixteenth century spread across Europe within a generation.

## Whose letters

It also made attribution difficult. A face appears in a printer’s book long after it was cut, under the printer’s name and not the cutter’s. Harry Carter’s survey of early typography set out the evidence available to sort these questions: specimen sheets, inventories, contracts and the books themselves [@carter1969]. Hendrik Vervliet spent decades applying it to the French Renaissance, comparing faces letter by letter across hundreds of books, and his catalogues redistributed many types long credited to Claude Garamont among his contemporaries, Robert Granjon and Pierre Haultin among them [@vervliet2008].

The name Garamond had a modern history of its own. When foundries in the early twentieth century revived “Garamond” for machine composition, several of them took as their model types that had been cut not by Garamont but by Jean Jannon, a printer at Sedan, some sixty years after Garamont’s death. Beatrice Warde pointed this out in 1926. The revivals kept the name, and many digital faces still carry it.

## What the reader sees

None of this is visible on the page as a reader meets it. The reader judges a book by how easily it reads. The bibliographer sees, in the same page, the trace of a punchcutter’s files, the wear of a matrix and the habits of the caster who justified it. Early printers bought their type by size names, pica, english, long primer, and judged it as the reader did, by the page it made [@gaskell1972]. The history of type begins in steel, but it was always written for that page.

## References {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 ───────────────────────────────────────────────────────────────────────
```

## アレンジ

### 全体で1つの一覧にする

すべての文献を巻末の1つの参考文献にまとめる出版社もあります。スコープを変え、3つの`## References`見出しを削除すると、一覧は最後の章のあとに続きます。

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

### APAで引用する

社会科学の論文集なら、スタイルを変えるだけで、ほかは何も変えません。

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

## よくあるつまずき

- **2回定義したキーは、何の知らせもなく最初の項目が使われる.** 参考文献は本全体で数えられます。2つの章に書いたキーは最初の定義が使われ、2つ目は警告なしに無視されます。複数の著者による本では、寄稿者ごとにキーに接頭辞を付けてください。
- **headingsオブジェクトを渡すとH1の改ページが消える.** 既定ではH1は奇数ページへ改ページします（always-odd）。ところがheadingsオブジェクトを渡すと中身にかかわらずこの既定がリセットされ、章は改ページせずに続けて組まれ、span: 'page'も効かなくなります。どの設定でもheadings.levels[0].breakBefore: { enabled: true, parity }を書き直してください。
- **buildBundleは章の間でメタデータを渡さない。{totalPages}は章ごと.** buildBundleはある章から次の章へメタデータを渡しません。{title}や{author}などは各章自身のフロントマターからしか得られないため、フロントマターのない章ではどれも空で印字されます（Sandboxは最初の章より後の章のフロントマターを捨てます）。また{totalPages}は各章のページ数を数えます。柱には本のタイトルを直接書いてください。
- **属性値には{も}も書けない。"を含む値は一重引用符で囲む.** 属性値は閉じ波かっこで終わるため、{や}を含められません。二重引用符を含む値は一重引用符で囲みます。ドル記号は問題ありません。
- **見出しスタイルはその階層の改ページを受け継ぐ.** headingStylesの項目は、指定しなかったフィールドをすべて見出しの階層から受け継ぎ、breakBeforeも例外ではありません。:::pagebreakのあとのH1に付けた目次や奥付のスタイルはparity 'odd'を受け継ぎ、白ページの後ろに置かれます。こうしたスタイルにはbreakBefore: { enabled: false }を指定してください。
- **レイアウトの前にすべてのフォントを読み込む.** レイアウトはブラウザーが読み込んだフォントで文字を計測し、その幅をキャッシュします。最初のビルドのあとに届いたフォントがあると改行位置が狂い、PDFも画面と一致しなくなります。すべてのウェイトとスタイルを先に読み込み、遅れて届いたときは再ビルドの前にclearMeasurementCache()を呼んでください。
- **フロントマターの値はすべて引用符で囲む.** YAMLはtitle: 1984を数値として、日付をDateオブジェクトとして読みます。文字列でない値はプレースホルダーに空で出力され、PDFにもタイトルが付きません。値はすべて引用符で囲んでください（title: "1984"）。
- **設定はオブジェクトの同一性でキャッシュされる。毎回新しいオブジェクトを作る.** エンジンは解決済みの設定をオブジェクトの同一性でキャッシュします。そのため、設定をその場で書き換えて再ビルドすると前の結果が再利用されます。ビルドのたびに新しいオブジェクトを作ってください。レシピの設定がファクトリー関数config()になっているのはこのためです。

- `scope: 'chapter'`が並べるのは、その文書が`:::bibliography`より前で引用したものであって、そのH1の節が引用したものではありません。すべての論考を1つのMarkdownファイルに入れると、2つ目の一覧は1つ目の論考の文献を繰り返し、3つ目は両方を繰り返します。
- 同じキーが複数あるときは、本全体で最初の定義が優先されます。2人の寄稿者が別々の文献に同じ`smith2001`を使うと、あとの論考は何の知らせもなく先の寄稿者の本を引用します。寄稿者ごとにキーに接頭辞を付けるか、一覧を原稿と照合してください。
- 同じ著者・同じ年の区別は本全体で決まります。別々の章にある、Smithの2001年の異なる2つの著作は、ある章の一覧にそのうち1つしかなくても、2001a、2001bと印字されることがあります。

## クレジット

- レシピ: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- 書体: Libre Caslon Text (OFL-1.1), Libre Caslon Display (OFL-1.1), IBM Plex Sans Condensed (OFL-1.1)
- コード: MIT · サンプルの内容: CC-BY-4.0

## 関連レシピ

- [No. 087 · APA 7で引用する博士論文の一章](https://postext.dev/ja/cookbook/apa-thesis-with-bibtex.md): [@key, p. 33]と書いた引用をciteproc-jsがAPA 7の形に整え、文献一覧をBibTeXのブロックから組む博士論文の一章です。 · 難易度 2 (中級) · 論文・学術書, 報告書
- [No. 030 · 著者名入りのアンソロジー](https://postext.dev/ja/cookbook/anthology-with-bylines.md): 歩くことをめぐる3編の随筆。見出しに著者・年・掲載誌を持たせ、章扉はその3つを、偶数ページの柱と目次は著者名を印字します。 · 難易度 2 (中級) · 小説・戯曲・文芸
- [No. 007 · 章ごとのファイルから1冊の本を組む](https://postext.dev/ja/cookbook/book-from-chapters.md): buildBundleで5つのMarkdownファイルを1冊の本として組みます。各章は奇数ページから始まり、ページ番号、章番号、図番号はファイルをまたいで続きます。 · 難易度 3 (上級) · マニュアル・ガイド・リファレンス
