In breve
Un piccolo campionario di una cartoleria. Ogni pagina dice su quale carta è stampata, e nel visualizzatore 3D ogni pagina ha davvero l’aspetto di quella carta e si gira come lei.
Che cosa comporrai
The Stock Book di Harwell & Daughters, un commerciante di carta di Bristol inventato: dodici pagine in formato 120 × 176 mm, una copertina in cartoncino e quattro fogli che descrivono ciascuno la carta su cui sono stampati. Il recto di un foglio è la scheda della carta, con grammatura, spessore, finitura, tinta e opacità; il verso è una pagina di testo composta come si usa quella carta: un romanzo, una lettera, una didascalia da galleria, un glossario. In piano sullo schermo le dodici pagine si stampano uguali. In Folio ogni foglio ha la sua carta: la velina color crema, il foglio vergato a coste, la patinata lucida che gira piatta e rigida, e la carta bibbia da 40 g che si piega subito e lascia trasparire il retro.
Questa ricetta risponde a
- Come stampo una copertina, un inserto o una sezione di tavole su una carta diversa dal resto del libro?
- Come mostro il mio documento come un libro 3D le cui pagine il lettore gira a mano?
La risposta breve
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(' · ')),
});
Ingredienti
- Funzionalità
- Una carta diversa per una sequenza di pagineVisualizzatore di libri in 3D (Folio)Carta, rilegatura, scrivania e luce (Folio)Stili di titoloAperture progettateAttributi dei titoliStili di paragrafoCampioni di coloreTestatine secondo il ruolo della paginaTavolozza di colori semanticaMargini speculariPagine su un canvas
- Usa anche
- Bibliografie e glossariInterruzioni di pagina e di colonnaGeometria della sezioneCapitoli non numerati
- Tipo
- Crimson Pro, Libre Caslon Display, IBM Plex Sans Condensed (SIL OFL 1.1)
- Risorse
- Nessuna: ogni immagine è disegnata nel codice
Preparazione
#1 · Un blocco per foglio
// :::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
Un blocco :::paper comincia su una nuova pagina e così fa il testo che lo segue, quindi qui ogni sequenza riempie un foglio: la scheda sul recto, un :::pagebreak, il saggio sul verso. Un foglio ha una sola carta per entrambe le facce, e prende quella del suo recto: una sequenza cominciata su un verso stamperebbe quella pagina sulla carta del foglio con cui la condivide (vedi Errori frequenti).
#2 · La carta del libro e le sue copertine
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 è la carta di ogni pagina fuori da un blocco; gli attributi di un blocco la sostituiscono campo per campo, e i valori predefiniti della carta riempiono il resto. cover: 'pages' fa della pagina 1 il piatto anteriore e della pagina 12 quello posteriore, così il libro resta chiuso finché non lo apri; questo richiede un numero pari di pagine, che l’interruzione parity: 'even' della copertina posteriore garantisce.
#3 · Copertine al vivo
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
];
Ogni copertina è uno stile di titolo il cui progetto grafico contiene un campo di colore grande quanto il formato rifilato. Lo stile azzera i margini per la durata della sua sezione, la sola pagina di copertina, così il campo raggiunge ogni bordo e le pagine che seguono conservano i loro margini.
#4 · La scheda della carta
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') };
Ogni foglio si apre con un titolo di primo livello disegnato da uno slot: l’occhiello dagli attributi del titolo, il nome della carta e un filetto sottile. Le righe delle specifiche sono uno stile di paragrafo nel carattere condensato delle etichette, con le etichette in grassetto nel colore d’accento.
#5 · Rileggi la carta dall’impaginazione
Il codice è nella risposta breve qui sopra. Ogni pagina impaginata dentro un blocco porta gli attributi del blocco in page.paper, così come sono scritti: { type: 'bible', grammage: 40 }. createFolioFromDocument li passa al visualizzatore, che disegna ogni foglio con la sua carta; il pen li legge anche per la riga di stato e per il testo alternativo di ogni pagina.
La ricetta completa
// ═══ 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`---Esempio Markdown · 190 righe · content.en.md
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(' · ')), }); // #endregionKit · core, fonts, viewer: uguale in ogni ricetta · 224 righe
// ─── 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 ───────────────────────────────────────────────────────────────────────
Lo script.js composto funziona così com’è: incollalo nello script modulo di qualsiasi pagina, oppure apri la ricetta su CodePen. Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)
Varianti
#Aggiungi una sezione di tavole su patinata più pesante
Un blocco dentro il testo di qualunque libro stampa quelle pagine sulla loro carta; le figure citate al suo interno vengono collocate prima che si chiuda.
+:::paper{type=coatedGloss grammage=170 showThrough=false}
+
+# Plates {kicker="Between pages 64 and 65"}
+
+::resource{id="plate-1"}
+
+:::#Un opuscolo spillato con copertina in cartoncino
La ricetta della fanzine avvolge pagine di carta da giornale in un foglio di copertina in cartoncino con due blocchi e una cucitura a punto metallico.
Errori frequenti
Errore frequente
Una sequenza di carta comincia e finisce su una nuova pagina, e un foglio prende la carta del suo recto
Il testo dentro :::paper comincia su una nuova pagina, e così anche il testo che lo segue. Un foglio ha due pagine e una sola carta, quella del suo recto, quindi una sequenza che comincia su un verso stampa quella pagina sulla carta del foglio di cui fa parte. Metti un’interruzione di pagina dove una sequenza deve cominciare su un recto. Una carta diversa per una sequenza di pagine →
Errore frequente
cover: 'pages' richiede un numero pari di pagine per il piatto posteriore
Con binding.cover impostato su 'pages' la prima pagina si gira come piatto anteriore e l’ultima come piatto posteriore solo quando cade su un verso. Un numero dispari di pagine lascia il libro con la copertina anteriore e senza quella posteriore: portalo a un numero pari oppure tieni la copertina rigida (case). Carta, rilegatura, scrivania e luce (Folio) →
Errore frequente
Il visualizzatore Folio prende le dimensioni del suo contenitore
createFolioFromDocument adatta il libro all’elemento che gli passi, con i pulsanti e il conteggio delle pagine nei margini. Un elemento senza altezza (un div vuoto nel flusso normale) non mostra nulla: dagli un’altezza in CSS. Visualizzatore di libri in 3D (Folio) →
Crediti
- Ricetta
- Ignacio Ferro
- Testo
- Testo originale, CC BY 4.0
- Font
- Crimson Pro (SIL OFL 1.1) · Libre Caslon Display (SIL OFL 1.1) · IBM Plex Sans Condensed (SIL OFL 1.1)
- Codice
- MIT, come Postext
Modifica questo testo ↗ (si apre in una nuova scheda)Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)


