# Un manuale i cui rimandi puntano alle pagine

> Un manuale d’uso per bicicletta i cui rimandi dicono «see section 3.2 on page 3», in avanti e indietro, con ogni numero e pagina presi dall’impaginazione.

- Versione HTML: https://postext.dev/it/cookbook/manual-see-page-references
- Ricetta Nº 092 · Struttura del libro · Livello 2 (Intermedio) · Output: Canvas, PDF
- Generi: Manuali, guide e opere di consultazione
- Richiede postext ≥ 1.28.0, postext-pdf ≥ 1.28.0 · provata con 1.31.0, postext-pdf 1.31.0 il 2026-10-13
- Pagine: [1](https://postext.dev/cookbook/manual-see-page-references/en/p01.webp?v=9cd5a2c0), [2](https://postext.dev/cookbook/manual-see-page-references/en/p02.webp?v=9cd5a2c0), [3](https://postext.dev/cookbook/manual-see-page-references/en/p03.webp?v=9cd5a2c0), [4](https://postext.dev/cookbook/manual-see-page-references/en/p04.webp?v=9cd5a2c0)
- PDF: https://postext.dev/cookbook/manual-see-page-references/en/manual-see-page-references.pdf?v=9cd5a2c0
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=manual-see-page-references&lang=en (.postext: https://postext.dev/cookbook/manual-see-page-references/en/manual-see-page-references.postext)
- Ultimo aggiornamento: 2026-10-11
- Altre lingue: [en](https://postext.dev/en/cookbook/manual-see-page-references.md), [es](https://postext.dev/es/cookbook/manual-see-page-references.md), [ca](https://postext.dev/ca/cookbook/manual-see-page-references.md), [de](https://postext.dev/de/cookbook/manual-see-page-references.md), [fr](https://postext.dev/fr/cookbook/manual-see-page-references.md), [pt](https://postext.dev/pt/cookbook/manual-see-page-references.md), [zh](https://postext.dev/zh/cookbook/manual-see-page-references.md), [ja](https://postext.dev/ja/cookbook/manual-see-page-references.md), [ar](https://postext.dev/ar/cookbook/manual-see-page-references.md)

## In breve

Un breve manuale d’uso per una bicicletta da turismo. Quando il testo ti manda a un’altra sezione, ne indica il numero e la pagina in cui si trova, ed entrambi sono link.

## Che cosa comporrai

Il manuale d’uso di una bicicletta da turismo inventata, la Ardena T3: quattro pagine A5 composte in Archivo, con il nome del modello in una fascia blu navy sulla prima pagina. Un manuale si legge in ordine sparso, quindi ogni istruzione che dipende da un’altra dice dove si trova: *see section 3.2 on page 3*. Chi scrive assegna un identificatore a un titolo, a un paragrafo o a una frase di un passaggio e scrive `:ref` dovunque il testo vi rimandi; Postext stampa il numero, il titolo o la pagina e ne fa un link. Il sommario in copertina si scrive allo stesso modo, così spostare una sezione, o tradurre il manuale e lasciarlo riscorrere, non lascia mai un numero sbagliato.

**Questa ricetta risponde a:**

- Come rimando a una sezione e alla pagina in cui si trova, e tengo entrambi giusti mentre il libro cambia?

## La risposta breve

```js
// script.js, righe 28–38
// In the Markdown, `## Brakes {#sec-brakes}` names a heading, `:anchor{#warn-pressure}`
// a point in a paragraph and `[close the lever]{#step-close-lever}` a phrase in a step.
// `:ref{id="sec-pad-wear"}` prints "section 3.2", `style=page` "page 3", `style=title`
// the heading's words and `style=pageNumber` the bare 3. The page numbers come from the
// previous layout pass: the engine lays the manual out again until they hold.
const crossRefs = {
  section: t({ en: 'section {n}', es: 'apartado {n}' }), // H2 and H3: "section 3.2"
  page: t({ en: 'page {n}', es: 'página {n}' }), // spelt out: a manual is read by anyone
};
// Every reference is a link in the PDF and on screen; this makes it look like one.
const references = { referenceColor: col('orange'), referenceBold: true };
```

## Ingredienti

**Insegna**

- [Rimandi](https://postext.dev/it/docs/document-format.md#rimandi-e-ancore): Riferimenti a un titolo, a un riquadro o a una frase segnata tramite il suo id, che ne stampano il numero, il titolo o la pagina e vi rimandano con un link; le pagine si assestano man mano che il libro cambia.

**Usa anche**

- [Titoli numerati](https://postext.dev/it/docs/configuration-text.md#sovrascritture-per-livello)
- [Elenchi puntati e liste di controllo](https://postext.dev/it/docs/configuration-text.md#elenchi-puntati)
- [Elenchi numerati](https://postext.dev/it/docs/configuration-text.md#elenchi-numerati)
- [Riquadri](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri)
- [Aperture progettate](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato)
- [Attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)
- [Testatine e folio](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Margini speculari](https://postext.dev/it/docs/configuration-page-layout.md#margini-speculari)
- [Tavolozza di colori semantica](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori)
- [Esportazione in PDF](https://postext.dev/it/docs/configuration-programmatic-usage.md#generare-pdf)
- [Citazioni che collocano le figure](https://postext.dev/it/docs/document-format.md#rimando-in-linea-la-forma-principale)
- [Testatine secondo il ruolo della pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Font incorporati nel PDF](https://postext.dev/it/docs/configuration-programmatic-usage.md#perché-un-provider-di-font)

**La configurazione in breve**

- [`bodyText`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), [`calloutStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri), [`colorPalette`](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori), [`crossRefs`](https://postext.dev/it/docs/configuration-notes-references.md#rimandi), [`footer`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`header`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`headings`](https://postext.dev/it/docs/configuration-text.md#titoli), [`layout`](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione), [`locale`](https://postext.dev/it/docs/configuration-text.md#sillabazione), [`orderedLists`](https://postext.dev/it/docs/configuration-text.md#elenchi-numerati), [`page`](https://postext.dev/it/docs/configuration-page-layout.md#pagina), [`paragraphStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo), [`unorderedLists`](https://postext.dev/it/docs/configuration-text.md#elenchi-puntati)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione), [`decompressWoff2`](https://postext.dev/it/docs/configuration-programmatic-usage.md#provider-di-font-per-il-browser-fontsource--woff2), [`renderPageToCanvas`](https://postext.dev/it/docs/configuration-programmatic-usage.md#rendere-una-pagina-come-bitmap), [`renderToPdf`](https://postext.dev/it/docs/configuration-programmatic-usage.md#generare-pdf)

**Caratteri**

- Archivo (OFL-1.1), Archivo Narrow (OFL-1.1)

## Preparazione

### 1 · Dai un nome a ciò a cui il testo rimanda

Il codice è nella [risposta breve](https://postext.dev/it/cookbook/manual-see-page-references.md#la-risposta-breve) qui sopra. I titoli prendono un identificatore Pandoc, `### Checking pad wear {#sec-pad-wear}`; l’avviso sulla pressione degli pneumatici comincia con `:anchor{#warn-pressure}`, un segno invisibile; l’ultimo passaggio del montaggio di una ruota è `[close the lever so that it points backwards]{#step-close-lever}`, che conserva le sue parole e dà loro un nome; e il riquadro si apre con `:::callout{#box-wet type="note" title="Wet weather"}`. Un rimando al passaggio stampa le parole del passaggio, un rimando al riquadro il suo titolo.

### 2 · Un solo bersaglio, quattro modi di nominarlo

`:ref{id="sec-pad-wear"}` stampa *section 3.2* con la parola di `crossRefs.section`; `style=page` stampa *page 3* con la parola di `crossRefs.page`; `style=title` stampa *Checking pad wear*; `style=pageNumber` stampa il semplice *3*. Il sommario della copertina combina il numero semplice, il titolo e la pagina semplice, così si legge come un indice. In spagnolo gli stessi rimandi stampano *apartado 3.2* e *página 3*, perché i modelli seguono la lingua dell’edizione.

### 3 · Le pagine si assestano da sole

Un numero di pagina si conosce solo dopo che il manuale è stato impaginato. Il motore lo impagina, legge la pagina in cui è finito ogni bersaglio e lo impagina di nuovo finché nessun rimando cambia, lo stesso ciclo che riempie un indice. Un testo che si allunga nella traduzione, o una sezione spostata all’inizio, cambia le pagine e i rimandi le seguono, in avanti e indietro.

### 4 · I numeri vengono dal contatore di livello 2

```js
// script.js, righe 42–47
const H2 = { level: 2, numberingTemplate: '{2}', numberSeparator: '   ', fontFamily: DISPLAY,
  fontWeight: 700, fontSize: pt(15), lineHeight: pt(LEAD * 1.5), color: col('navy'),
  marginTop: pt(LEAD), marginBottom: pt(LEAD / 3) };
const H3 = { level: 3, numberingTemplate: '{2}.{3}', numberSeparator: '  ', fontFamily: DISPLAY,
  fontWeight: 700, fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'),
  marginTop: pt(LEAD / 2), marginBottom: pt(LEAD / 4) };
```

Il manuale ha un solo titolo di livello 1, il nome del modello in copertina, quindi le sue sezioni si numerano dal livello 2: `{2}` dà da 1 a 5 e `{2}.{3}` dà 1.1, 1.2.

### 5 · La copertina è l’apertura di quel titolo

```js
// script.js, righe 51–73
const BAND = 104; // mm from the trim's top
const label = { fontFamily: DISPLAY, fontWeight: 700, textTransform: 'uppercase',
  letterSpacing: pt(1.6), align: 'left' };
const cover = {
  enabled: true,
  minHeight: mm(BAND - PAGE.top + 8), // the contents start 8 mm under the band
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('navy') },
      placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } },
    { kind: 'box', id: 'stripe', style: { backgroundColor: col('orange') },
      placement: { ...at('page', 'top-left', 0, BAND), size: { width: 'fill', height: mm(2.5) } } },
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...label, fontSize: pt(9),
      color: col('tint'), placement: at('container', 'top-left', 0, 6) },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontWeight: 700,
      fontSize: pt(58), lineHeight: 1, color: col('paper'), align: 'left',
      placement: at('container', 'top-left', -1, 30) },
    { kind: 'text', id: 'subtitle', content: '{attr.subtitle}', fontFamily: TEXT,
      fontSize: pt(11), color: col('paper'), align: 'left',
      placement: at('container', 'top-left', 0, 55) },
    { kind: 'text', id: 'edition', content: '{attr.edition}', ...label, fontSize: pt(7.5),
      color: col('tint'), placement: at('container', 'top-left', 0, 74) },
  ] },
};
```

La fascia è alta 104 mm, metà della pagina, e una striscia arancione la chiude. Occhiello, sottotitolo e riga dell’edizione sono attributi del titolo, quindi l’edizione spagnola li cambia nel proprio testo.

```js
// script.js, righe 13–20
const palette = {
  ink: '#1b1f24', navy: '#1d2c3c', orange: '#b8471b', tint: '#f7e7de', rule: '#d5cfc8',
  muted: '#5c636b', paper: '#ffffff',
};
// Each colour carries its hex and the palette entry it follows.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.orange })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
```

## La ricetta completa

Un unico file, composto a partire dalla cartella della ricetta con il testo di esempio e il kit comune del Ricettario già inclusi; costruisce la propria pagina. Per eseguirlo, mettilo in uno `<script type="module">` di una pagina vuota, oppure incollalo nel pannello JS di un nuovo pen di CodePen (come modulo). Importa postext da esm.sh: non c’è niente da installare né da compilare.

- Cartella della ricetta: https://github.com/drnachio/postext/tree/main/cookbook/manual-see-page-references

### script.js

```js
// ═══ Postext Cookbook · Nº 092 · A manual whose references point to pages ══════════
// https://postext.dev/en/cookbook/manual-see-page-references
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Archivo, Archivo Narrow (SIL OFL 1.1) · Needs postext ≥ 1.28.0
import { buildDocumentWithFonts, renderPageToCanvas } from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'manual-see-page-references';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a navy cover band and one signal orange for numbers and references
const palette = {
  ink: '#1b1f24', navy: '#1d2c3c', orange: '#b8471b', tint: '#f7e7de', rule: '#d5cfc8',
  muted: '#5c636b', paper: '#ffffff',
};
// Each colour carries its hex and the palette entry it follows.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.orange })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const TEXT = 'Archivo', DISPLAY = 'Archivo Narrow';
const PAGE = { w: 148, h: 210, top: 20, bottom: 20, inner: 18, outer: 15 }; // mm: A5, mirrored
const LEAD = 13; // pt
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });

// #region answer: the words a reference prints, and references that are links
// In the Markdown, `## Brakes {#sec-brakes}` names a heading, `:anchor{#warn-pressure}`
// a point in a paragraph and `[close the lever]{#step-close-lever}` a phrase in a step.
// `:ref{id="sec-pad-wear"}` prints "section 3.2", `style=page` "page 3", `style=title`
// the heading's words and `style=pageNumber` the bare 3. The page numbers come from the
// previous layout pass: the engine lays the manual out again until they hold.
const crossRefs = {
  section: t({ en: 'section {n}', es: 'apartado {n}' }), // H2 and H3: "section 3.2"
  page: t({ en: 'page {n}', es: 'página {n}' }), // spelt out: a manual is read by anyone
};
// Every reference is a link in the PDF and on screen; this makes it look like one.
const references = { referenceColor: col('orange'), referenceBold: true };
// #endregion

// #region numbers: sections 1–5 and 1.1, 1.2… under the one level-1 heading
const H2 = { level: 2, numberingTemplate: '{2}', numberSeparator: '   ', fontFamily: DISPLAY,
  fontWeight: 700, fontSize: pt(15), lineHeight: pt(LEAD * 1.5), color: col('navy'),
  marginTop: pt(LEAD), marginBottom: pt(LEAD / 3) };
const H3 = { level: 3, numberingTemplate: '{2}.{3}', numberSeparator: '  ', fontFamily: DISPLAY,
  fontWeight: 700, fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'),
  marginTop: pt(LEAD / 2), marginBottom: pt(LEAD / 4) };
// #endregion

// #region cover: the model name large in a navy band over half the first page
const BAND = 104; // mm from the trim's top
const label = { fontFamily: DISPLAY, fontWeight: 700, textTransform: 'uppercase',
  letterSpacing: pt(1.6), align: 'left' };
const cover = {
  enabled: true,
  minHeight: mm(BAND - PAGE.top + 8), // the contents start 8 mm under the band
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('navy') },
      placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } },
    { kind: 'box', id: 'stripe', style: { backgroundColor: col('orange') },
      placement: { ...at('page', 'top-left', 0, BAND), size: { width: 'fill', height: mm(2.5) } } },
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...label, fontSize: pt(9),
      color: col('tint'), placement: at('container', 'top-left', 0, 6) },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontWeight: 700,
      fontSize: pt(58), lineHeight: 1, color: col('paper'), align: 'left',
      placement: at('container', 'top-left', -1, 30) },
    { kind: 'text', id: 'subtitle', content: '{attr.subtitle}', fontFamily: TEXT,
      fontSize: pt(11), color: col('paper'), align: 'left',
      placement: at('container', 'top-left', 0, 55) },
    { kind: 'text', id: 'edition', content: '{attr.edition}', ...label, fontSize: pt(7.5),
      color: col('tint'), placement: at('container', 'top-left', 0, 74) },
  ] },
};
// #endregion

const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body', ...label, fontSize: pt(7.5),
  letterSpacing: pt(1.1), color: col('muted'), placement: at('page', edge, x, 10), ...extra,
});

const config = () => ({
  locale: t({ en: 'en-gb', es: 'es' }), // picks the hyphenation patterns
  colorPalette,
  crossRefs,
  page: { sizePreset: 'custom', width: mm(PAGE.w), height: mm(PAGE.h), dpi: 150,
    margins: { top: mm(PAGE.top), bottom: mm(PAGE.bottom), left: mm(PAGE.inner),
      right: mm(PAGE.outer), mirror: true } },
  layout: { layoutType: 'single' },
  bodyText: { fontFamily: TEXT, fontSize: pt(9.3), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), ...references,
    textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true,
    hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true },
  headings: { fontFamily: DISPLAY, color: col('ink'), fontWeight: 700, levels: [
    // A new page on either side: the default would wait for a recto.
    { level: 1, fontSize: pt(58), breakBefore: { enabled: true, parity: 'any' },
      advancedDesign: cover },
    H2, H3,
  ] },
  orderedLists: { numberFormat: 'arabic', separator: '', fontFamily: DISPLAY, fontWeight: 700,
    color: col('orange'), gap: mm(2.5), itemSpacing: pt(2),
    marginTop: pt(LEAD / 3), marginBottom: pt(LEAD / 3) },
  unorderedLists: { color: col('orange'), gap: mm(2), marginTop: pt(LEAD / 3),
    marginBottom: pt(LEAD / 3) },
  calloutStyles: [
    { id: 'note', title: '', backgroundEnabled: true, background: col('tint'),
      stripe: { enabled: true, side: 'left', width: pt(3), color: col('orange') },
      padding: { top: mm(2.5), right: mm(3), bottom: mm(2.5), left: mm(3.5) },
      titleStyle: { fontFamily: DISPLAY, fontWeight: 700, fontSize: pt(9), color: col('orange'),
        textTransform: 'uppercase', letterSpacing: pt(1.2) },
      marginTop: pt(LEAD / 2), marginBottom: pt(LEAD / 2) },
  ],
  paragraphStyles: [
    { id: 'colophon', fontSize: pt(7), lineHeight: pt(9.5), color: col('muted'),
      marginTop: pt(LEAD / 2) },
  ],
  header: { elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', PAGE.outer, { color: col('orange') }),
    head('verso-title', '{title}', 'even', 'top-left', PAGE.outer + 8),
    head('recto-title', '{title}', 'odd', 'top-right', -(PAGE.outer + 8), { align: 'right' }),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -PAGE.outer,
      { align: 'right', color: col('orange') }),
  ] },
  footer: { elements: [] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Ardena T3 · Owner’s manual"
author: "Ardena Cycles"
---

# Ardena T3 {#manual kicker="Owner’s manual" subtitle="Touring bicycle · frame sizes 50 to 60 cm" edition="Edition 2 · Read before the first ride"}

This manual covers the T3 as it leaves the shop: a steel touring frame, 700C wheels, cable disc brakes and a 2 × 10 drivetrain. Read :ref{id="sec-first-ride"} before you ride. The rest of the manual is arranged by part of the bicycle, and every reference to another section gives its page.

- **:ref{id="sec-first-ride" style=number}** :ref{id="sec-first-ride" style=title}, page :ref{id="sec-first-ride" style=pageNumber}
- **:ref{id="sec-wheels" style=number}** :ref{id="sec-wheels" style=title}, page :ref{id="sec-wheels" style=pageNumber}
- **:ref{id="sec-brakes" style=number}** :ref{id="sec-brakes" style=title}, page :ref{id="sec-brakes" style=pageNumber}
- **:ref{id="sec-drivetrain" style=number}** :ref{id="sec-drivetrain" style=title}, page :ref{id="sec-drivetrain" style=pageNumber}
- **:ref{id="sec-schedule" style=number}** :ref{id="sec-schedule" style=title}, page :ref{id="sec-schedule" style=pageNumber}

## Before your first ride {#sec-first-ride}

### Checks before every ride {#sec-checks}

A check takes two minutes and finds most faults before they find you. Lift each end of the bicycle and spin the wheel: it should run true and stop slowly, without rubbing. Squeeze each brake lever; it should stop well short of the handlebar. If a lever reaches the bar, do not ride: see :ref{id="sec-pad-wear"} on :ref{id="sec-pad-wear" style=page}.

Press each tyre hard with your thumb. A tyre that gives easily needs air; the pressures are in :ref{id="sec-pressure"}, :ref{id="sec-pressure" style=page}. Finally, check that both quick-release levers are closed and point backwards, as :ref{id="step-close-lever"} in :ref{id="sec-front-wheel"} describes.

### Setting the saddle height {#sec-saddle}

1. Sit on the saddle with one heel on the pedal at the bottom of its stroke.
2. Raise or lower the saddle until that leg is straight, without rocking your hips.
3. Tighten the seat-post bolt to 5 N·m with a torque wrench. Never raise the saddle above the **MIN INSERT** mark on the post.

## Wheels and tyres {#sec-wheels}

### Tyre pressure {#sec-pressure}

The tyres are 700 × 38 mm. Run them between 3.5 and 5 bar: the lower figure for a rider under 70 kg or for gravel, the higher one for a loaded bicycle on tarmac. Check the pressure once a week with a gauge; a tyre loses about half a bar a week through the tube alone.

:anchor{#warn-pressure}Never pump a tyre above 5 bar. The rim and the tyre are rated for that pressure together, and a tyre that leaves the rim at speed throws the rider.

### Removing and fitting the front wheel {#sec-front-wheel}

1. Shift to the smallest rear sprocket, so the chain is slack if you remove the rear wheel later.
2. Open the quick-release lever, no tools needed, and unscrew the nut on the other side by three turns.
3. Lift the front of the bicycle and let the wheel drop out of the fork.

To fit it, reverse the steps. Seat the axle fully in both fork ends, tighten the nut until the lever meets resistance halfway through its swing, then [close the lever so that it points backwards]{#step-close-lever}. With the wheel back, squeeze the front brake twice; if the lever feels soft, the pads may have moved, and :ref{id="sec-brake-cable"} on :ref{id="sec-brake-cable" style=page} explains how to set them again.

## Brakes {#sec-brakes}

### How the brakes work

The T3 has mechanical disc brakes. Each lever pulls a cable that pushes one pad against a steel rotor; the second pad is fixed, and the rotor flexes towards it. New pads need bedding in: ride at walking pace, brake firmly to a stop ten times on each wheel, and the pads will reach full power.

:::callout{#box-wet type="note" title="Wet weather"}
Disc brakes stop in the rain, but the first turn of the wheel only clears water from the rotor. Brake a little earlier than usual, and keep oil and chain lube away from the rotors: a contaminated pad cannot be cleaned and must be replaced.
:::

### Checking pad wear {#sec-pad-wear}

Look into the caliper from above with a torch. Each pad has a steel backing plate and a layer of braking material. Replace both pads of a wheel when the material is thinner than 1 mm, or when a scraping noise tells you the backing plate has reached the rotor. Fit pads of the same type: the T3 uses resin pads, code AR-P2.

After fitting new pads, adjust the cable as in :ref{id="sec-brake-cable"} and bed them in as described on :ref{id="sec-brakes" style=page}.

### Adjusting the cable {#sec-brake-cable}

As the pads wear, the lever travels further. Turn the barrel adjuster at the lever anticlockwise, half a turn at a time, until the lever stops about 2 cm from the bar. If the adjuster runs out of thread, screw it back in, loosen the cable clamp bolt on the caliper, pull 2 mm of cable through and tighten the clamp bolt to 6 N·m. Then squeeze the lever hard ten times and check its travel again.

## Gears and chain {#sec-drivetrain}

### Cleaning and oiling the chain {#sec-chain}

A clean chain lasts three times as long as a dirty one. Every 500 km, or after any wet ride, wipe the chain with a dry cloth while you turn the pedals backwards. Put one drop of chain oil on each roller, turn the pedals for a minute, and wipe off what the outside of the chain still carries. Oil on the outside picks up grit; oil inside the rollers does the work. Keep the oil off the rotors, as :ref{id="box-wet" style=title} on :ref{id="box-wet" style=page} explains.

## Maintenance schedule {#sec-schedule}

- **Before every ride:** the checks in :ref{id="sec-checks"}, page :ref{id="sec-checks" style=pageNumber}.
- **Every week:** tyre pressure, :ref{id="sec-pressure" style=page}.
- **Every 500 km:** clean and oil the chain, :ref{id="sec-chain" style=page}.
- **Every 1,000 km:** pad wear and cable travel, :ref{id="sec-brakes"} from :ref{id="sec-pad-wear" style=page}; the seat-post bolt, :ref{id="sec-saddle" style=page}.
- **Every 3,000 km:** chain wear, and a full service at an Ardena dealer.

The pressure warning on :ref{id="warn-pressure" style=page} applies to the replacement tyres as well.

:::paragraphs{style="colophon"}
A fictional bicycle; sample text for the Postext Cookbook (CC BY 4.0). Archivo and Archivo Narrow (SIL OFL).
:::
`;

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
// Every face the design uses.
const FONTS = {
  Archivo: ['400', '400i', '700'],
  'Archivo Narrow': ['700'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const doc = await buildDocumentWithFonts({ markdown }, config(), kitFonts(FONTS));
const title = t({ en: 'A manual whose references point to pages',
  es: 'Un manual cuyas remisiones apuntan a páginas' });
showPages(doc, { title });
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);

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

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

// ─── Kit · fonts 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 · pdf v3 ── the same in every recipe that exports a PDF
/** The Fontsource files the screen used, as TrueType, with their range and
 *  place: the nearest weight the family ships, upright with no italic;
 *  latin, then what its letters need (kitSubsetsFor). */
async function fontsourceProvider(family, weight, style, request) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  const weights = meta?.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style;
  const text = String.fromCodePoint(...(request?.codePoints ?? []));
  const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => {
    const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`);
    if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`);
    return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())),
      { unicodeRange: kitRanges()[subset], order });
  }));
  return files.length === 1 ? files[0] : files;
}

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

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

## Varianti

### Scrivi «p.» nei rimandi

Un libro di consultazione con molti rimandi alle pagine risparmia spazio con l’abbreviazione.

```diff
-  page: t({ en: 'page {n}', es: 'página {n}' }), // spelt out: a manual is read by anyone
+  page: t({ en: 'p. {n}', es: 'pág. {n}' }),
```

### Lascia i rimandi in nero

Quando la maggior parte della pagina è fatta di rimandi, tienili nel colore del testo e in grassetto; restano comunque link nel PDF e a schermo.

```diff
-const references = { referenceColor: col('orange'), referenceBold: true };
+const references = { referenceColor: col('ink'), referenceBold: true };
```

## Errori frequenti

- Un rimando a un `:anchor` senza stile stampa la pagina, perché un’ancora invisibile non ha parole proprie. Usa `[words]{#id}` quando il rimando deve citare il testo.
- Un identificatore deve essere unico nel libro. I rimandi raggiungono il primo `{#sec-brakes}`, e il pannello Verifiche della Sandbox elenca il secondo come `duplicateAnchor`.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Caratteri: Archivo (OFL-1.1), Archivo Narrow (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 094 · Un libro tecnico con rimandi tra capitoli](https://postext.dev/it/cookbook/technical-book-crossref-chapters.md): Un manuale solare di tre capitoli il cui testo scrive :sun, :loads e :array-size, e buildBundle stampa ogni numero e pagina tra i capitoli. · Livello 3 (Avanzato) · Manuali, guide e opere di consultazione, Libri di testo
- [Nº 126 · Un indice giapponese in ordine gojūon, per lettura](https://postext.dev/it/cookbook/gojuon-index.md): Un capitolo sui termini della composizione giapponese e il suo indice: ogni segno dà la lettura (furigana o yomi), le voci vanno sotto あ行, か行, さ行 in ordine JIS. · Livello 2 (Intermedio) · Manuali, guide e opere di consultazione, Libri di testo
- [Nº 007 · Un libro da capitoli separati](https://postext.dev/it/cookbook/book-from-chapters.md): buildBundle compone cinque file Markdown come un solo libro: ogni capitolo si apre su un recto e i numeri di pagina, capitolo e figura proseguono tra i file. · Livello 3 (Avanzato) · Manuali, guide e opere di consultazione
