# Ein Papiermusterbuch, jedes Blatt auf eigenem Papier

> Jedes Blatt im Musterbuch eines Papierhändlers steht in einem Block :::paper; postext-folio zeichnet es mit Ton, Grammatur, Oberfläche, Steifigkeit der Sorte.

- HTML-Fassung: https://postext.dev/de/cookbook/paper-swatch-book
- Rezept Nº 101 · Ausgabe und Integration · Stufe 2 (Mittel) · Ausgaben: Canvas, Live-Steuerung
- Gattungen: Kataloge
- Benötigt postext ≥ 1.25.0 · getestet mit 1.31.0 am 2026-10-13
- Seiten: [1](https://postext.dev/cookbook/paper-swatch-book/en/p01.webp?v=580a62b8), [2](https://postext.dev/cookbook/paper-swatch-book/en/p02.webp?v=580a62b8), [3](https://postext.dev/cookbook/paper-swatch-book/en/p03.webp?v=580a62b8), [4](https://postext.dev/cookbook/paper-swatch-book/en/p04.webp?v=580a62b8), [5](https://postext.dev/cookbook/paper-swatch-book/en/p05.webp?v=580a62b8), [6](https://postext.dev/cookbook/paper-swatch-book/en/p06.webp?v=580a62b8), [7](https://postext.dev/cookbook/paper-swatch-book/en/p07.webp?v=580a62b8), [8](https://postext.dev/cookbook/paper-swatch-book/en/p08.webp?v=580a62b8), [9](https://postext.dev/cookbook/paper-swatch-book/en/p09.webp?v=580a62b8), [10](https://postext.dev/cookbook/paper-swatch-book/en/p10.webp?v=580a62b8), [11](https://postext.dev/cookbook/paper-swatch-book/en/p11.webp?v=580a62b8), [12](https://postext.dev/cookbook/paper-swatch-book/en/p12.webp?v=580a62b8)
- In der Sandbox öffnen: https://postext.dev/de/sandbox#recipe=paper-swatch-book&lang=en (.postext: https://postext.dev/cookbook/paper-swatch-book/en/paper-swatch-book.postext)
- Zuletzt aktualisiert: 2026-10-10
- Andere Sprachen: [en](https://postext.dev/en/cookbook/paper-swatch-book.md), [es](https://postext.dev/es/cookbook/paper-swatch-book.md), [ca](https://postext.dev/ca/cookbook/paper-swatch-book.md), [fr](https://postext.dev/fr/cookbook/paper-swatch-book.md), [it](https://postext.dev/it/cookbook/paper-swatch-book.md), [pt](https://postext.dev/pt/cookbook/paper-swatch-book.md), [zh](https://postext.dev/zh/cookbook/paper-swatch-book.md), [ja](https://postext.dev/ja/cookbook/paper-swatch-book.md), [ar](https://postext.dev/ar/cookbook/paper-swatch-book.md)

## Kurz gesagt

Ein kleines Musterbuch aus einem Papierladen. Jede Seite sagt, auf welchem Papier sie gedruckt ist, und im 3D-Betrachter sieht jede Seite wirklich so aus und blättert sich so wie dieses Papier.

## Was Sie setzen

*The Stock Book* von Harwell & Daughters, einem erfundenen Papierhändler aus Bristol: zwölf Seiten im Endformat 120 × 176 mm, ein Kartonumschlag und vier Blätter, von denen jedes das Papier beschreibt, auf dem es gedruckt ist. Die Vorderseite eines Blatts ist die Karte der Sorte mit Grammatur, Volumen, Oberfläche, Ton und Opazität; die Rückseite ist eine Seite Text, gesetzt so, wie dieses Papier verwendet wird: ein Roman, ein Brief, eine Ausstellungsbeschriftung, ein Glossar. Flach auf dem Bildschirm sehen die zwölf Seiten gleich aus. In Folio hat jedes Blatt sein Papier: das cremefarbene Velinpapier, das gerippte Vergé, das Glanzpapier, das flach und steif umschlägt, und das Dünndruckpapier mit 40 g, das sofort umfällt und seine Rückseite durchscheinen lässt.

**Dieses Rezept beantwortet:**

- Wie drucke ich Umschlag, Beilage oder Bildteil auf einem anderen Papier als den Rest des Buchs?
- Wie zeige ich mein Dokument als 3D-Buch, dessen Seiten der Leser von Hand umblättert?

## Die kurze Antwort

```js
// script.js, Zeilen 349–362
const stocks = doc.pages.map((page) => page.paper?.type ?? 'the book’s paper');
const stage = document.createElement('section');
stage.id = 'folio';
stage.ariaLabel = 'The stock book in 3D';
stage.style.cssText = 'height: min(78vh, 720px); margin: 0 auto; max-width: 1280px';
document.getElementById('pages').before(stage);
createFolioFromDocument(stage, doc, {
  appearance: { textureBaseUrl: 'https://postext.dev/folio/textures' },
  alt: (i) => `Page ${i + 1}, printed on ${stocks[i]}`,
  // page.paper is the fence's attributes as written: { type: 'bible', grammage: 40 }.
  onChange: ({ pages }) => kitStatus(pages.map((i) => `p. ${i + 1}: ${stocks[i]}`
    + (doc.pages[i].paper?.grammage ? ` ${doc.pages[i].paper.grammage} g/m²` : ''))
    .join(' · ')),
});
```

## Zutaten

**Zeigt**

- [Ein anderes Papier für eine Seitenfolge](https://postext.dev/de/docs/document-format.md#paper): Ein :::paper-Container druckt die Seiten, die er enthält, auf ihrem eigenen Papier: ein Tafelteil auf Glanzpapier, ein Kartonumschlag, ein paar getönte Blätter.
- [3D-Buchbetrachter (Folio)](https://postext.dev/de/docs/configuration-programmatic-usage.md#ein-3d-buch-postext-folio): postext-folio öffnet ein gesetztes Dokument als gedrucktes Buch auf einem Schreibtisch: Doppelseiten nach der Recto-Regel und Blätter, die der Leser von Hand umblättert.

**Verwendet außerdem**

- [Papier, Bindung, Tisch und Licht (Folio)](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#folio-betrachter-konfiguration)
- [Überschriftenformate](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Gestaltete Auftakte](https://postext.dev/de/docs/configuration-text.md#seitenbreite-und-erweiterte-gestaltung)
- [Überschriftenattribute](https://postext.dev/de/docs/document-format.md#attribute-von-überschriften)
- [Absatzformate](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Farbfelder](https://postext.dev/de/docs/document-format.md#inline-formatierung)
- [Kopfzeilen nach Seitenrolle](https://postext.dev/de/docs/configuration-page-layout.md#textelemente)
- [Semantische Farbpalette](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette)
- [Gespiegelte Ränder](https://postext.dev/de/docs/configuration-page-layout.md#gespiegelte-ränder)
- [Seiten auf einem Canvas](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern)
- [Literaturverzeichnisse und Glossare](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Seiten- und Spaltenumbrüche](https://postext.dev/de/docs/document-format.md#pagebreak)
- [Abschnittsgeometrie](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Kapitel ohne Nummer](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)

**Die Konfiguration auf einen Blick**

- [`bodyText`](https://postext.dev/de/docs/configuration-text.md#fließtext), [`colorPalette`](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette), `folio`, [`footer`](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen), [`header`](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen), [`headingStyles`](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile), [`headings`](https://postext.dev/de/docs/configuration-text.md#überschriften), [`layout`](https://postext.dev/de/docs/configuration-page-layout.md#layout), [`locale`](https://postext.dev/de/docs/configuration-text.md#silbentrennung), [`page`](https://postext.dev/de/docs/configuration-page-layout.md#seite), [`paragraphStyles`](https://postext.dev/de/docs/configuration-styles.md#absatzformate)

**APIs**

- [`buildDocumentWithFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden), [`createFolioFromDocument`](https://postext.dev/de/docs/configuration-programmatic-usage.md#ein-3d-buch-postext-folio), [`renderPageToCanvas`](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern)

**Schriften**

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

## Zubereitung

### 1 · Ein Block je Blatt

```js
// script.js, Zeilen 137–334
// :::paper{type=bookWove grammage=80}
// ## Book Wove, Natural {kicker="Stock 1 of 4"}
// … the stock card, a :::pagebreak, the specimen page …
// :::
// A fence starts and ends on a new page; each run here fills one leaf, recto and verso
// (gotcha: paper-run-whole-leaves). Canvas, HTML and PDF ignore the stock; Folio draws it.
const markdown = String.raw`---
title: "The Stock Book"
subtitle: "Four papers, each printed on itself"
author: "Harwell & Daughters, Paper Merchants"
---

:::paper{type=cardStock grammage=300 shade=#f7f3ea}

# The Stock Book {style="cover" kicker="Harwell & Daughters · Paper Merchants" season="Autumn 2026 · Four stocks on four leaves"}

# How to use this book {kicker="Before you begin"}

Every leaf in this book is printed on the paper it describes. The front of each leaf names the stock and gives its numbers; the back carries a page of type set the way that paper is most often used, so you can judge it as a reader will meet it.

Turn the leaves slowly. A heavy sheet turns in a wide, stiff curve, and a light one falls over almost at once. Hold a page up to the window and you will see how much of the reverse shows through. Run a thumb along the edge and you will feel the coating, or the lack of it.

The numbers on each card are the ones a printer asks for. Weight is in grams per square metre. Bulk, in cubic centimetres per gram, says how much thickness a gram of the paper gives you: multiply the two and you have the caliper of a single sheet in microns, and from that the spine of a book.

The covers are our house board, a 300 g uncoated card that takes a crease without cracking. We use it for the covers of this book and for most of the menus and programmes our customers print with us.

:::

:::paper{type=bookWove grammage=80}

# Book Wove, Natural {kicker="Stock 1 of 4"}

:::paragraphs{style="spec"}
**Weight** 80 g/m²

**Bulk** 1.6 cm³/g · a sheet of 128 µm

**Finish** uncoated, with a soft wove surface

**Shade** natural, a warm cream

**Opacity** 94 %
:::

A bulky, lightweight sheet made for novels. It is thicker than its weight suggests, so a short book still feels substantial in the hand, and its cream shade is kinder to the eye over a long evening than a bright white.

The wove surface comes from the woven wire the sheet forms on: an even, faint texture you see only at a low angle. Ink sits into it rather than on it, and type keeps a little softness at its edges.

**Best for** fiction, memoir, poetry, any book that is read rather than consulted.

:::pagebreak

## Specimen

The ferry left the north bank at six every evening, whatever the weather, and for thirty-one years Tobias Wren had written its passengers into a ledger bound in green cloth. He wrote the date, the hour and the state of the river, and then a line for every soul who came aboard: a name if they gave one, a description if they did not.

The river at Hallam is four hundred yards wide and never still. It comes down from the moors brown with peat, carrying branches and once, in the flood of the year his daughter was born, the roof of a barn with a goat standing on it. Tobias took the goat across for nothing and wrote it down as a passenger.

On the evening of the last crossing the light was going early. October had turned cold all at once, and the willows on the south bank had dropped their leaves in a single night, so that the landing stage looked bare and new.

:::

:::paper{type=uncoated grammage=100 texture=laid textureStrength=1.5 shade=#fbf8f1}

# Laid Writing, White {kicker="Stock 2 of 4"}

:::paragraphs{style="spec"}
**Weight** 100 g/m²

**Bulk** 1.25 cm³/g · a sheet of 125 µm

**Finish** uncoated, with laid and chain lines

**Shade** a soft, unbleached white

**Opacity** 96 %
:::

A writing paper with a ribbed surface. Close laid lines run across the sheet and wider chain lines cross them, the pattern a dandy roll presses into the wet web. Tilt the page under a lamp and the ribs catch the light.

It is a paper for letters and announcements more than for books: firm enough to stand in an envelope, rough enough to take a fountain pen without feathering, and quiet enough to print a single paragraph on and leave the rest of the sheet empty.

**Best for** letterheads, invitations, certificates, the endpapers of a fine edition.

:::pagebreak

## Specimen

*Harwell & Daughters, 14 Rope Walk, Bristol*

Dear Mrs Okafor,

Thank you for your letter and the proofs of the wedding stationery, which arrived this morning in perfect order. We agree that the laid sheet suits the invitation far better than the smooth one; the lines give the lettering something to rest on.

We have set aside two reams from the same making, so that the reply cards and the order of service will match the invitation exactly. Papers from different makings can differ by a shade, and on a table set side by side the difference shows.

The envelopes will follow on Thursday. If you would like the flaps lined, we keep a tissue in four colours and would be glad to send samples.

With our best wishes for the day,

*Ruth Harwell*

:::

:::paper{type=coatedGloss grammage=150 showThrough=false}

# Art Gloss, Bright White {kicker="Stock 3 of 4"}

:::paragraphs{style="spec"}
**Weight** 150 g/m²

**Bulk** 0.8 cm³/g · a sheet of 120 µm

**Finish** coated both sides, calendered to a gloss

**Shade** bright white

**Opacity** 98 %
:::

A heavy coated sheet, polished between steel rollers until it shines. The clay coating fills the hollows between the fibres, so ink stays on the surface and dries as a sharp, saturated dot. Photographs come up with deep blacks and clean highlights.

Because the coating is calendered hard, the sheet is thinner for its weight than an uncoated one, and noticeably stiffer. It turns as a single flat plane and lands with a sound. Nothing shows through from the reverse.

**Best for** art books, exhibition catalogues, posters, the plate section of a history.

:::pagebreak

## Specimen

*Plate 14.* Evening light on the estuary at Pill, looking west towards the two bridges. The photograph was taken from the old pilots’ steps an hour before high water, when the mud is still uncovered and holds the colour of the sky.

*Plate 15.* A barge loaded with Norwegian timber passes under the suspension bridge in 1931. Glass negative, the Harwell family collection; printed here at the size of the original plate.

On this paper the four process inks print as they will in the book: :swatch{color="cyan"} cyan, :swatch{color="magenta"} magenta, :swatch{color="yellow"} yellow and :swatch{color="key"} black, side by side and never muddied by the sheet.

Ask for the plate section of a book to be printed on gloss and bound in among the uncoated pages of the text: the reader will feel the change of paper at every turn.

:::

:::paper{type=bible grammage=40}

# India Bible, Opaque {kicker="Stock 4 of 4"}

:::paragraphs{style="spec"}
**Weight** 40 g/m²

**Bulk** 1.1 cm³/g · a sheet of 44 µm

**Finish** uncoated, with a fine vellum tooth

**Shade** an ivory white

**Opacity** 82 %
:::

The thinnest sheet we stock that will still go through a book press. A thousand pages of it make a spine of under five centimetres, which is why dictionaries, prayer books and the collected works of the long-winded are printed on it.

It is strong for its weight, but it is not opaque. Hold this leaf up and you will read the type on its back through the sheet. Good setting on a thin paper prints the lines of both sides in register, so that the show-through falls between the lines and not across them.

**Best for** dictionaries, scriptures, reference works, timetables, pocket editions.

:::pagebreak

## Specimen

:::paragraphs{style="entry"}
**caliper** *n.* The thickness of a single sheet of paper or board, measured in microns. From the instrument used to measure it.

**chain lines** *n. pl.* The widely spaced lines of a laid paper, left by the wires that hold the laid wires of the mould in place.

**deckle** *n.* The rough, feathered edge of a handmade sheet, where the pulp ran under the frame of the mould.

**grain** *n.* The direction in which most fibres in a sheet lie. A book folds and opens well when the grain runs parallel to its spine.

**opacity** *n.* The degree to which a sheet hides what is printed on its reverse.

**ream** *n.* Five hundred sheets of paper, once four hundred and eighty.
:::

:::

:::paper{type=cardStock grammage=300 shade=#f7f3ea}

# Ordering {kicker="Harwell & Daughters"}

Every stock in this book is held in our warehouse in full reams and in the sizes most printers use. Samples of any paper are free; send us the job and we will send the sheets. Orders placed before noon leave the same day.

When you write, give the weight and the name as they appear on the cards. If you are matching an earlier job, send us a sheet of it: we will find the making it came from.

:::paragraphs{style="colophon"}
Set in Crimson Pro, Libre Caslon Display and IBM Plex Sans Condensed (SIL Open Font License) · Text: original, CC BY 4.0 · Harwell & Daughters is a fictional merchant.
:::

# Harwell & Daughters {style="back" address="14 Rope Walk · Bristol · since 1897"}

:::
`; // content.<lang>.md, inlined by the Cookbook
```

Ein Block `:::paper` beginnt auf einer neuen Seite, und der Text danach ebenfalls; jeder Abschnitt füllt hier also ein Blatt: die Karte auf der rechten Seite, ein `:::pagebreak`, die Probeseite auf der linken. Ein Blatt hat für beide Seiten dasselbe Papier, und zwar das seiner rechten Seite: Ein Abschnitt, der auf einer linken Seite begänne, würde diese Seite auf der Sorte des Blatts drucken, das sie teilt (siehe Häufige Fehler).

### 2 · Das eigene Papier des Buchs und seine Umschlagseiten

```js
// script.js, Zeilen 41–51
const folio = {
  tilt: 24,
  // The paper of any page outside a :::paper fence (there are none here). Each fence's
  // attributes override these field by field, and past them the stock's defaults apply.
  paper: { type: 'uncoated' },
  // 'pages': page 1 turns as the front board and page 12, a verso, as the back board, so no
  // case is drawn round the block (gotcha: folio-cover-pages-even).
  binding: { type: 'paperback', cover: 'pages', coverMaterial: 'paper' },
  surface: { type: 'linen' },
  lighting: { environment: 'daylight', intensity: 1 },
};
```

`folio.paper` ist das Papier jeder Seite außerhalb eines Blocks; die Attribute eines Blocks überschreiben es Feld für Feld, und die Voreinstellungen der Sorte ergänzen den Rest. `cover: 'pages'` macht Seite 1 zum vorderen und Seite 12 zum hinteren Deckel, sodass das Buch geschlossen daliegt, bis Sie es öffnen; dafür ist eine gerade Seitenzahl nötig, die der Umbruch `parity: 'even'` des hinteren Umschlags garantiert.

### 3 · Umschlagseiten bis in den Anschnitt

```js
// script.js, Zeilen 55–84
const field = { kind: 'box', id: 'field', style: { backgroundColor: col('oxblood') },
  placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: mm(TRIM_H) } } };
const front = { enabled: true, minHeight: mm(TRIM_H), slot: { elements: [field,
  text('kicker', '{attr.kicker}', { ...label, fontSize: pt(7.5), color: col('cream') },
    at('page', 'top-left', INNER, 22)),
  text('title', '{titleText}', { fontFamily: 'Libre Caslon Display', fontSize: pt(52),
    lineHeight: 0.95, color: col('cream'), align: 'left' },
  { ...at('#kicker', 'below', 0, 30), size: { width: mm(TRIM_W - INNER - OUTER) } }),
  text('subtitle', '{subtitle}', { fontFamily: 'Crimson Pro', italic: true,
    fontSize: pt(12), lineHeight: 1.3, color: col('cream'), align: 'left' },
  { ...at('#title', 'below', 0, 6), size: { width: mm(80) } }),
  text('season', '{attr.season}', { ...label, fontSize: pt(7), color: col('cream') },
    at('page', 'bottom-left', INNER, -18)),
] } };
const back = { enabled: true, minHeight: mm(TRIM_H), slot: { elements: [field,
  text('name', '{titleText}', { fontFamily: 'Libre Caslon Display', fontSize: pt(20),
    lineHeight: 1, color: col('cream'), align: 'center' },
  { ...at('page', 'center', 0, -4), size: { width: mm(90) } }),
  text('address', '{attr.address}', { ...label, fontSize: pt(6.8), color: col('cream'),
    align: 'center' }, { ...at('#name', 'below', 0, 4), size: { width: mm(90) } }),
] } };
// The covers are the book's two openers; the folio skips them (pages: 'body', below).
// No margins: the design's column is the whole page, so the field reaches every edge, and the
// style's margins last only as long as its section, the cover page.
const flush = { span: 'page', margins: { top: mm(0), bottom: mm(0), left: mm(0), right: mm(0) } };
const headingStyles = [
  { id: 'cover', numbered: false, toc: false, advancedDesign: front, ...flush },
  { id: 'back', numbered: false, toc: false, advancedDesign: back, ...flush,
    breakBefore: { enabled: true, parity: 'even' } }, // the last page, a verso
];
```

Jeder Umschlag ist ein Überschriftenformat, dessen Design eine Farbfläche in der Größe des Endformats enthält. Das Format setzt seine Ränder für die Länge seines Abschnitts, also allein für die Umschlagseite, auf null, sodass die Fläche jede Kante erreicht und die folgenden Seiten ihre Ränder behalten.

### 4 · Die Karte der Sorte

```js
// script.js, Zeilen 88–99
const card = { enabled: true, minHeight: mm(25), slot: { elements: [
  text('kicker', '{attr.kicker}', { ...label, fontSize: pt(7), color: col('oxblood') },
    at('container', 'top-left', 0, 2)),
  text('name', '{titleText}', { fontFamily: 'Libre Caslon Display', fontSize: pt(24),
    lineHeight: 1.05, color: col('ink'), align: 'left' },
  { ...at('#kicker', 'below', 0, 2.5), size: { width: 'fill' } }),
  { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.5), color: col('rule'),
    placement: { ...at('#name', 'below', 0, 3.5), size: { width: 'fill' } } },
] } };
const spec = { id: 'spec', fontFamily: 'IBM Plex Sans Condensed', fontSize: pt(8),
  lineHeight: pt(LEAD * 0.85), textAlign: 'left', firstLineIndent: pt(0), color: col('ink'),
  boldColor: col('oxblood') };
```

Jedes Blatt beginnt mit einer Überschrift der Ebene 1, die aus einem Platz gezeichnet wird: die Dachzeile aus den Attributen der Überschrift, der Name der Sorte und eine Haarlinie. Die Zeilen mit den technischen Angaben sind ein Absatzformat in der schmalen Labelschrift, mit fetten Labels in der Akzentfarbe.

### 5 · Das Papier aus dem Layout zurücklesen

Der Code steht oben in [der kurzen Antwort](https://postext.dev/de/cookbook/paper-swatch-book.md#die-kurze-antwort). Jede Seite, die innerhalb eines Blocks gesetzt wird, trägt die Attribute des Blocks in `page.paper`, so wie sie geschrieben sind: `{ type: 'bible', grammage: 40 }`. `createFolioFromDocument` reicht sie an den Betrachter weiter, der jedes Blatt mit seiner Sorte zeichnet; der Pen liest sie außerdem für die Statuszeile und für die Textalternative jeder Seite.

## Das ganze Rezept

Eine Datei, aus dem Ordner des Rezepts zusammengesetzt, mit dem Beispieltext und dem gemeinsamen Baukasten der Rezepte bereits eingebettet; sie baut ihre eigene Seite auf. Zum Ausführen setzen Sie sie in ein `<script type="module">` auf einer leeren Seite oder fügen sie in das JS-Feld eines neuen CodePen-Pens ein (als Modul). Sie importiert postext von esm.sh, es gibt also nichts zu installieren oder zu bauen.

- Quellordner: https://github.com/drnachio/postext/tree/main/cookbook/paper-swatch-book

### script.js

```js
// ═══ Postext Cookbook · Nº 101 · A swatch book, every leaf on its own paper ═══════════
// https://postext.dev/en/cookbook/paper-swatch-book
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Crimson Pro, Libre Caslon Display, IBM Plex Sans Cond. (OFL) · Needs postext ≥ 1.25.0
//
// A paper merchant's stock book: each leaf sits inside a :::paper fence that names its stock,
// and postext-folio draws that leaf with the stock's shade, surface, stiffness and opacity.
// Turn the leaves and compare: the gloss sheet turns flat and stiff, the bible paper falls
// over at once and shows its back through, the card covers turn as boards.
import { buildDocumentWithFonts, renderPageToCanvas } from 'https://esm.sh/postext';
import { createFolioFromDocument } from 'https://esm.sh/postext-folio';

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

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: the house colours, and the four process inks the gloss page shows
const palette = {
  ink: '#22201d', // text
  oxblood: '#5e2128', // the covers' colour field, kickers and folios
  cream: '#f3ead6', // type on the covers
  rule: '#cbc2b3', // hairlines
  muted: '#6f675e', // labels, folios, the colophon
  cyan: '#00a0e0', magenta: '#e0007a', yellow: '#ffe100', key: '#1b1b1b', // process inks
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.oxblood, model: 'hex' } },
];
// #endregion
const [TRIM_W, TRIM_H, TOP, BOTTOM, INNER, OUTER] = [120, 176, 18, 20, 16, 14]; // mm
const LEAD = 13.4; // pt
const label = { fontFamily: 'IBM Plex Sans Condensed', fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', align: 'left' };
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const text = (id, content, look, placement) => ({ kind: 'text', id, content, overflow: 'wrap',
  placement, ...look });

// #region folio: the book's own paper and binding; the leaves' papers come from the text
const folio = {
  tilt: 24,
  // The paper of any page outside a :::paper fence (there are none here). Each fence's
  // attributes override these field by field, and past them the stock's defaults apply.
  paper: { type: 'uncoated' },
  // 'pages': page 1 turns as the front board and page 12, a verso, as the back board, so no
  // case is drawn round the block (gotcha: folio-cover-pages-even).
  binding: { type: 'paperback', cover: 'pages', coverMaterial: 'paper' },
  surface: { type: 'linen' },
  lighting: { environment: 'daylight', intensity: 1 },
};
// #endregion

// #region covers: a colour field to the bleed, for the front and the back of the book
const field = { kind: 'box', id: 'field', style: { backgroundColor: col('oxblood') },
  placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: mm(TRIM_H) } } };
const front = { enabled: true, minHeight: mm(TRIM_H), slot: { elements: [field,
  text('kicker', '{attr.kicker}', { ...label, fontSize: pt(7.5), color: col('cream') },
    at('page', 'top-left', INNER, 22)),
  text('title', '{titleText}', { fontFamily: 'Libre Caslon Display', fontSize: pt(52),
    lineHeight: 0.95, color: col('cream'), align: 'left' },
  { ...at('#kicker', 'below', 0, 30), size: { width: mm(TRIM_W - INNER - OUTER) } }),
  text('subtitle', '{subtitle}', { fontFamily: 'Crimson Pro', italic: true,
    fontSize: pt(12), lineHeight: 1.3, color: col('cream'), align: 'left' },
  { ...at('#title', 'below', 0, 6), size: { width: mm(80) } }),
  text('season', '{attr.season}', { ...label, fontSize: pt(7), color: col('cream') },
    at('page', 'bottom-left', INNER, -18)),
] } };
const back = { enabled: true, minHeight: mm(TRIM_H), slot: { elements: [field,
  text('name', '{titleText}', { fontFamily: 'Libre Caslon Display', fontSize: pt(20),
    lineHeight: 1, color: col('cream'), align: 'center' },
  { ...at('page', 'center', 0, -4), size: { width: mm(90) } }),
  text('address', '{attr.address}', { ...label, fontSize: pt(6.8), color: col('cream'),
    align: 'center' }, { ...at('#name', 'below', 0, 4), size: { width: mm(90) } }),
] } };
// The covers are the book's two openers; the folio skips them (pages: 'body', below).
// No margins: the design's column is the whole page, so the field reaches every edge, and the
// style's margins last only as long as its section, the cover page.
const flush = { span: 'page', margins: { top: mm(0), bottom: mm(0), left: mm(0), right: mm(0) } };
const headingStyles = [
  { id: 'cover', numbered: false, toc: false, advancedDesign: front, ...flush },
  { id: 'back', numbered: false, toc: false, advancedDesign: back, ...flush,
    breakBefore: { enabled: true, parity: 'even' } }, // the last page, a verso
];
// #endregion

// #region stock-card: the kicker over each stock's name, and the specification lines
const card = { enabled: true, minHeight: mm(25), slot: { elements: [
  text('kicker', '{attr.kicker}', { ...label, fontSize: pt(7), color: col('oxblood') },
    at('container', 'top-left', 0, 2)),
  text('name', '{titleText}', { fontFamily: 'Libre Caslon Display', fontSize: pt(24),
    lineHeight: 1.05, color: col('ink'), align: 'left' },
  { ...at('#kicker', 'below', 0, 2.5), size: { width: 'fill' } }),
  { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.5), color: col('rule'),
    placement: { ...at('#name', 'below', 0, 3.5), size: { width: 'fill' } } },
] } };
const spec = { id: 'spec', fontFamily: 'IBM Plex Sans Condensed', fontSize: pt(8),
  lineHeight: pt(LEAD * 0.85), textAlign: 'left', firstLineIndent: pt(0), color: col('ink'),
  boldColor: col('oxblood') };
// #endregion

const config = () => ({
  locale: 'en-us',
  colorPalette,
  page: { sizePreset: 'custom', width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
    margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
      mirror: true } },
  layout: { layoutType: 'single' },
  bodyText: { fontFamily: 'Crimson Pro', fontSize: pt(10.2), lineHeight: pt(LEAD),
    color: col('ink'), boldColor: col('ink'), italicColor: col('ink'),
    referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(4),
    indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true },
  headings: { fontFamily: 'Libre Caslon Display', color: col('ink'), fontWeight: 400, levels: [
    // A :::paper fence already starts a new page, so the stock name breaks to any side.
    { level: 1, fontSize: pt(24), breakBefore: { enabled: true, parity: 'any' },
      marginTop: pt(0), marginBottom: pt(LEAD / 2), advancedDesign: card },
    { level: 2, fontFamily: 'IBM Plex Sans Condensed', fontSize: pt(7.5), fontWeight: 600,
      letterSpacing: pt(1.2), textTransform: 'uppercase', color: col('muted'),
      lineHeight: pt(LEAD), marginTop: pt(0), marginBottom: pt(LEAD / 2) },
  ] },
  headingStyles,
  paragraphStyles: [spec,
    { id: 'entry', fontSize: pt(8.4), lineHeight: pt(11.5), textAlign: 'left',
      firstLineIndent: pt(0), hangingIndent: mm(4), marginTop: pt(3) },
    { id: 'colophon', fontFamily: 'IBM Plex Sans Condensed', fontSize: pt(6.8),
      lineHeight: pt(9.4), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0),
      marginTop: pt(2 * LEAD) }],
  header: { elements: [] },
  footer: { elements: [text('folio', '{pageNumber} · {title}', { ...label, fontSize: pt(6.5),
    color: col('muted'), align: 'center', pages: 'body' }, at('page', 'bottom', 0, -10))] },
  folio,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
// #region paper-runs: each leaf's stock is a :::paper fence around its two pages
// :::paper{type=bookWove grammage=80}
// ## Book Wove, Natural {kicker="Stock 1 of 4"}
// … the stock card, a :::pagebreak, the specimen page …
// :::
// A fence starts and ends on a new page; each run here fills one leaf, recto and verso
// (gotcha: paper-run-whole-leaves). Canvas, HTML and PDF ignore the stock; Folio draws it.
const markdown = String.raw`---
title: "The Stock Book"
subtitle: "Four papers, each printed on itself"
author: "Harwell & Daughters, Paper Merchants"
---

:::paper{type=cardStock grammage=300 shade=#f7f3ea}

# The Stock Book {style="cover" kicker="Harwell & Daughters · Paper Merchants" season="Autumn 2026 · Four stocks on four leaves"}

# How to use this book {kicker="Before you begin"}

Every leaf in this book is printed on the paper it describes. The front of each leaf names the stock and gives its numbers; the back carries a page of type set the way that paper is most often used, so you can judge it as a reader will meet it.

Turn the leaves slowly. A heavy sheet turns in a wide, stiff curve, and a light one falls over almost at once. Hold a page up to the window and you will see how much of the reverse shows through. Run a thumb along the edge and you will feel the coating, or the lack of it.

The numbers on each card are the ones a printer asks for. Weight is in grams per square metre. Bulk, in cubic centimetres per gram, says how much thickness a gram of the paper gives you: multiply the two and you have the caliper of a single sheet in microns, and from that the spine of a book.

The covers are our house board, a 300 g uncoated card that takes a crease without cracking. We use it for the covers of this book and for most of the menus and programmes our customers print with us.

:::

:::paper{type=bookWove grammage=80}

# Book Wove, Natural {kicker="Stock 1 of 4"}

:::paragraphs{style="spec"}
**Weight** 80 g/m²

**Bulk** 1.6 cm³/g · a sheet of 128 µm

**Finish** uncoated, with a soft wove surface

**Shade** natural, a warm cream

**Opacity** 94 %
:::

A bulky, lightweight sheet made for novels. It is thicker than its weight suggests, so a short book still feels substantial in the hand, and its cream shade is kinder to the eye over a long evening than a bright white.

The wove surface comes from the woven wire the sheet forms on: an even, faint texture you see only at a low angle. Ink sits into it rather than on it, and type keeps a little softness at its edges.

**Best for** fiction, memoir, poetry, any book that is read rather than consulted.

:::pagebreak

## Specimen

The ferry left the north bank at six every evening, whatever the weather, and for thirty-one years Tobias Wren had written its passengers into a ledger bound in green cloth. He wrote the date, the hour and the state of the river, and then a line for every soul who came aboard: a name if they gave one, a description if they did not.

The river at Hallam is four hundred yards wide and never still. It comes down from the moors brown with peat, carrying branches and once, in the flood of the year his daughter was born, the roof of a barn with a goat standing on it. Tobias took the goat across for nothing and wrote it down as a passenger.

On the evening of the last crossing the light was going early. October had turned cold all at once, and the willows on the south bank had dropped their leaves in a single night, so that the landing stage looked bare and new.

:::

:::paper{type=uncoated grammage=100 texture=laid textureStrength=1.5 shade=#fbf8f1}

# Laid Writing, White {kicker="Stock 2 of 4"}

:::paragraphs{style="spec"}
**Weight** 100 g/m²

**Bulk** 1.25 cm³/g · a sheet of 125 µm

**Finish** uncoated, with laid and chain lines

**Shade** a soft, unbleached white

**Opacity** 96 %
:::

A writing paper with a ribbed surface. Close laid lines run across the sheet and wider chain lines cross them, the pattern a dandy roll presses into the wet web. Tilt the page under a lamp and the ribs catch the light.

It is a paper for letters and announcements more than for books: firm enough to stand in an envelope, rough enough to take a fountain pen without feathering, and quiet enough to print a single paragraph on and leave the rest of the sheet empty.

**Best for** letterheads, invitations, certificates, the endpapers of a fine edition.

:::pagebreak

## Specimen

*Harwell & Daughters, 14 Rope Walk, Bristol*

Dear Mrs Okafor,

Thank you for your letter and the proofs of the wedding stationery, which arrived this morning in perfect order. We agree that the laid sheet suits the invitation far better than the smooth one; the lines give the lettering something to rest on.

We have set aside two reams from the same making, so that the reply cards and the order of service will match the invitation exactly. Papers from different makings can differ by a shade, and on a table set side by side the difference shows.

The envelopes will follow on Thursday. If you would like the flaps lined, we keep a tissue in four colours and would be glad to send samples.

With our best wishes for the day,

*Ruth Harwell*

:::

:::paper{type=coatedGloss grammage=150 showThrough=false}

# Art Gloss, Bright White {kicker="Stock 3 of 4"}

:::paragraphs{style="spec"}
**Weight** 150 g/m²

**Bulk** 0.8 cm³/g · a sheet of 120 µm

**Finish** coated both sides, calendered to a gloss

**Shade** bright white

**Opacity** 98 %
:::

A heavy coated sheet, polished between steel rollers until it shines. The clay coating fills the hollows between the fibres, so ink stays on the surface and dries as a sharp, saturated dot. Photographs come up with deep blacks and clean highlights.

Because the coating is calendered hard, the sheet is thinner for its weight than an uncoated one, and noticeably stiffer. It turns as a single flat plane and lands with a sound. Nothing shows through from the reverse.

**Best for** art books, exhibition catalogues, posters, the plate section of a history.

:::pagebreak

## Specimen

*Plate 14.* Evening light on the estuary at Pill, looking west towards the two bridges. The photograph was taken from the old pilots’ steps an hour before high water, when the mud is still uncovered and holds the colour of the sky.

*Plate 15.* A barge loaded with Norwegian timber passes under the suspension bridge in 1931. Glass negative, the Harwell family collection; printed here at the size of the original plate.

On this paper the four process inks print as they will in the book: :swatch{color="cyan"} cyan, :swatch{color="magenta"} magenta, :swatch{color="yellow"} yellow and :swatch{color="key"} black, side by side and never muddied by the sheet.

Ask for the plate section of a book to be printed on gloss and bound in among the uncoated pages of the text: the reader will feel the change of paper at every turn.

:::

:::paper{type=bible grammage=40}

# India Bible, Opaque {kicker="Stock 4 of 4"}

:::paragraphs{style="spec"}
**Weight** 40 g/m²

**Bulk** 1.1 cm³/g · a sheet of 44 µm

**Finish** uncoated, with a fine vellum tooth

**Shade** an ivory white

**Opacity** 82 %
:::

The thinnest sheet we stock that will still go through a book press. A thousand pages of it make a spine of under five centimetres, which is why dictionaries, prayer books and the collected works of the long-winded are printed on it.

It is strong for its weight, but it is not opaque. Hold this leaf up and you will read the type on its back through the sheet. Good setting on a thin paper prints the lines of both sides in register, so that the show-through falls between the lines and not across them.

**Best for** dictionaries, scriptures, reference works, timetables, pocket editions.

:::pagebreak

## Specimen

:::paragraphs{style="entry"}
**caliper** *n.* The thickness of a single sheet of paper or board, measured in microns. From the instrument used to measure it.

**chain lines** *n. pl.* The widely spaced lines of a laid paper, left by the wires that hold the laid wires of the mould in place.

**deckle** *n.* The rough, feathered edge of a handmade sheet, where the pulp ran under the frame of the mould.

**grain** *n.* The direction in which most fibres in a sheet lie. A book folds and opens well when the grain runs parallel to its spine.

**opacity** *n.* The degree to which a sheet hides what is printed on its reverse.

**ream** *n.* Five hundred sheets of paper, once four hundred and eighty.
:::

:::

:::paper{type=cardStock grammage=300 shade=#f7f3ea}

# Ordering {kicker="Harwell & Daughters"}

Every stock in this book is held in our warehouse in full reams and in the sizes most printers use. Samples of any paper are free; send us the job and we will send the sheets. Orders placed before noon leave the same day.

When you write, give the weight and the name as they appear on the cards. If you are matching an earlier job, send us a sheet of it: we will find the making it came from.

:::paragraphs{style="colophon"}
Set in Crimson Pro, Libre Caslon Display and IBM Plex Sans Condensed (SIL Open Font License) · Text: original, CC BY 4.0 · Harwell & Daughters is a fictional merchant.
:::

# Harwell & Daughters {style="back" address="14 Rope Walk · Bristol · since 1897"}

:::
`; // content.<lang>.md, inlined by the Cookbook
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Crimson Pro': ['400', '400i', '700'],
  'Libre Caslon Display': ['400'],
  'IBM Plex Sans Condensed': ['400', '600', '700'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const doc = await buildDocumentWithFonts({ markdown }, config(), kitFonts(FONTS));
showPages(doc, { title: 'The Stock Book · four papers in Folio' });

// #region answer: every page carries its stock; the viewer draws each leaf with it
const stocks = doc.pages.map((page) => page.paper?.type ?? 'the book’s paper');
const stage = document.createElement('section');
stage.id = 'folio';
stage.ariaLabel = 'The stock book in 3D';
stage.style.cssText = 'height: min(78vh, 720px); margin: 0 auto; max-width: 1280px';
document.getElementById('pages').before(stage);
createFolioFromDocument(stage, doc, {
  appearance: { textureBaseUrl: 'https://postext.dev/folio/textures' },
  alt: (i) => `Page ${i + 1}, printed on ${stocks[i]}`,
  // page.paper is the fence's attributes as written: { type: 'bible', grammage: 40 }.
  onChange: ({ pages }) => kitStatus(pages.map((i) => `p. ${i + 1}: ${stocks[i]}`
    + (doc.pages[i].paper?.grammage ? ` ${doc.pages[i].paper.grammage} g/m²` : ''))
    .join(' · ')),
});
// #endregion

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Varianten

### Einen Bildteil auf schwererem Glanzpapier einfügen

Ein Block im Text eines beliebigen Buchs druckt diese Seiten auf ihrer eigenen Sorte; die darin zitierten Abbildungen werden platziert, bevor er schließt.

```diff
+:::paper{type=coatedGloss grammage=170 showThrough=false}
+
+# Plates {kicker="Between pages 64 and 65"}
+
+::resource{id="plate-1"}
+
+:::
```

### Ein geheftetes Heft mit Kartonumschlag

[Das Zine-Rezept](https://postext.dev/de/cookbook/saddle-stitched-card-cover.md) legt mit zwei Blöcken und einer Rückstichheftung einen Kartonumschlag um Seiten aus Zeitungspapier.

## Häufige Fehler

- **Ein Papierwechsel beginnt und endet auf einer neuen Seite, und ein Blatt übernimmt das Papier seiner rechten Seite.** Text in :::paper beginnt auf einer neuen Seite, und der Text danach ebenfalls. Ein Blatt hat zwei Seiten und ein Papier, das seiner rechten Seite. Ein Abschnitt, der auf einer linken Seite beginnt, druckt diese Seite daher auf dem Papier des Blattes, das sie teilt. Setzen Sie einen Seitenumbruch dort, wo ein Abschnitt auf einer rechten Seite beginnen soll.
- **Cover: 'pages' braucht eine gerade Seitenzahl für den hinteren Deckel.** Mit binding.cover auf 'pages' blättert die erste Seite als vorderer Deckel und die letzte Seite nur dann als hinterer Deckel, wenn sie auf eine linke Seite (verso) fällt. Eine ungerade Seitenzahl lässt das Buch mit Vorderdeckel und ohne Rückdeckel: Füllen Sie auf eine gerade Zahl auf oder behalten Sie die Einbanddecke.
- **Der Folio-Betrachter übernimmt die Größe seines Containers.** createFolioFromDocument passt das Buch in das Element ein, das Sie übergeben, mit den Schaltflächen und der Seitenzahl in seinen Rändern. Ein Element ohne Höhe (ein leeres div im normalen Fluss) zeigt nichts: Geben Sie ihm in CSS eine Höhe.

## Nachweise

- Rezept: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Schrift: Crimson Pro (OFL-1.1), Libre Caslon Display (OFL-1.1), IBM Plex Sans Condensed (OFL-1.1)
- Code: MIT · Beispielinhalt: CC-BY-4.0

## Verwandte Rezepte

- [Nº 103 · Ein Prüfstand für die Einstellungen des Folio-Betrachters](https://postext.dev/de/cookbook/folio-settings-bench.md): Ein Formular mit allen Einstellungen von config.folio steuert per setAppearance einen Betrachter: Papier, Bindung, Einband, Tisch, Licht, Neigung. · Stufe 2 (Mittel) · Handbücher und Nachschlagewerke
- [Nº 026 · Ein Schriftmuster, alle Schriften vor dem Satz geladen](https://postext.dev/de/cookbook/fonts-before-layout.md): Ein vierseitiges Schriftmuster, einmal zu früh gebaut, als Beweis, dann mit jeder Schrift aus Konfiguration und Text geladen, bevor eine Zeile gemessen wird. · Stufe 2 (Mittel) · Kataloge
- [Nº 034 · Katalogeinträge gegenüber ihren Tafeln](https://postext.dev/de/cookbook/catalogue-facing-plates.md): Jeder Eintrag beginnt links und nennt seine Tafel im ersten Satz; die Tafel gleitet auf die rechte Seite gegenüber, skaliert auf den Satzspiegel. · Stufe 3 (Fortgeschritten) · Kataloge
