# دليل استخدام تشير إحالاته إلى الصفحات

> دليل مالك دراجة تقول إحالاته «see section 3.2 on page 3»، إلى الأمام وإلى الخلف، وكل رقم وصفحة فيها مأخوذ من الإخراج.

- نسخة HTML: https://postext.dev/ar/cookbook/manual-see-page-references
- وصفة رقم 092 · بنية الكتاب · المستوى 2 (متوسط) · المخرجات: Canvas, PDF
- الأنواع: الأدلة والمراجع
- تتطلب postext ≥ 1.12.0, postext-pdf ≥ 1.12.0 · اختُبرت مع 1.12.0, postext-pdf 1.12.0 بتاريخ 2026-10-01
- الصفحات: [1](https://postext.dev/cookbook/manual-see-page-references/en/p01.webp?v=191a53bf), [2](https://postext.dev/cookbook/manual-see-page-references/en/p02.webp?v=191a53bf), [3](https://postext.dev/cookbook/manual-see-page-references/en/p03.webp?v=191a53bf), [4](https://postext.dev/cookbook/manual-see-page-references/en/p04.webp?v=191a53bf)
- PDF: https://postext.dev/cookbook/manual-see-page-references/en/manual-see-page-references.pdf?v=191a53bf
- افتح في Sandbox: https://postext.dev/ar/sandbox#recipe=manual-see-page-references&lang=en (.postext: https://postext.dev/cookbook/manual-see-page-references/en/manual-see-page-references.postext)
- آخر تحديث: 2026-10-01
- لغات أخرى: [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), [zh](https://postext.dev/zh/cookbook/manual-see-page-references.md)

## باختصار

دليل مالك قصير لدراجة سياحية. حين يحيلك النص إلى قسم آخر يذكر رقم ذلك القسم والصفحة التي هو فيها، وكلاهما رابط.

## ما الذي ستنضده

دليل المالك لدراجة سياحية متخيَّلة، Ardena T3: أربع صفحات A5 منضدة بخط Archivo، واسم الطراز في شريط كحلي فوق الصفحة الأولى. الدليل يُقرأ بلا ترتيب، لذا تذكر كل تعليمة تعتمد على أخرى مكانها: *see section 3.2 on page 3*. يسمّي الكاتب عنوانًا أو فقرة أو عبارة في خطوة بمعرّف، ويكتب `:ref` حيثما أشار النص إليه؛ فيطبع Postext الرقم أو العنوان أو الصفحة ويجعله رابطًا. وتُكتب قائمة المحتويات على الغلاف بالطريقة نفسها، فنقل قسم من مكانه، أو ترجمة الدليل وتركه يعيد التدفق، لا يترك رقمًا قديمًا أبدًا.

**تجيب هذه الوصفة عن:**

- كيف أحيل إلى قسم والصفحة التي هو فيها، وأبقي الاثنين صحيحين كلما تغيّر الكتاب؟

## الجواب المختصر

```js
// script.js, سطرًا 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 };
```

## المكونات

**تعلّم**

- [الإحالات](https://postext.dev/ar/docs/document-format.md#الإحالات-والمراسي): إحالات إلى عنوان أو إطار أو عبارة معلَّمة بمعرّفها، تطبع رقمه أو عنوانه أو صفحته وترتبط به؛ وتستقر الصفحات كلما تغيّر الكتاب.

**تستخدم أيضًا**

- [العناوين المرقّمة](https://postext.dev/ar/docs/configuration.md#تجاوزات-كل-مستوى)
- [القوائم النقطية وقوائم التحقق](https://postext.dev/ar/docs/configuration.md#القوائم-غير-المرقّمة)
- [القوائم المرقّمة](https://postext.dev/ar/docs/configuration.md#القوائم-المرقّمة)
- [إطارات التنبيه](https://postext.dev/ar/docs/configuration.md#أنماط-الإطارات)
- [صفحات افتتاح مصمَّمة](https://postext.dev/ar/docs/configuration.md#الامتداد-والتصميم-المتقدم)
- [سمات العنوان](https://postext.dev/ar/docs/document-format.md#سمات-العناوين)
- [الترويسات وأرقام الصفحات](https://postext.dev/ar/docs/configuration.md#رؤوس-الصفحات-وتذييلاتها)
- [هوامش متناظرة](https://postext.dev/ar/docs/configuration.md#الهوامش-المتناظرة)
- [لوحة ألوان دلالية](https://postext.dev/ar/docs/configuration.md#لوحة-الألوان)
- [التصدير إلى PDF](https://postext.dev/ar/docs/configuration.md#توليد-ملفات-pdf)
- citations
- [الترويسات بحسب دور الصفحة](https://postext.dev/ar/docs/configuration.md#عناصر-النص)
- [أنماط الفقرات](https://postext.dev/ar/docs/configuration.md#أنماط-الفقرات)
- [الخطوط المضمَّنة في PDF](https://postext.dev/ar/docs/configuration.md#لماذا-مزوّد-الخطوط)

**الإعدادات في لمحة**

- [`bodyText`](https://postext.dev/ar/docs/configuration.md#نص-المتن), [`calloutStyles`](https://postext.dev/ar/docs/configuration.md#أنماط-الإطارات), [`colorPalette`](https://postext.dev/ar/docs/configuration.md#لوحة-الألوان), [`crossRefs`](https://postext.dev/ar/docs/configuration.md#الإحالات-المرجعية), [`footer`](https://postext.dev/ar/docs/configuration.md#رؤوس-الصفحات-وتذييلاتها), [`header`](https://postext.dev/ar/docs/configuration.md#رؤوس-الصفحات-وتذييلاتها), [`headings`](https://postext.dev/ar/docs/configuration.md#العناوين), [`layout`](https://postext.dev/ar/docs/configuration.md#التخطيط), [`locale`](https://postext.dev/ar/docs/configuration.md#تقسيم-الكلمات-بالواصلة), [`orderedLists`](https://postext.dev/ar/docs/configuration.md#القوائم-المرقّمة), [`page`](https://postext.dev/ar/docs/configuration.md#الصفحة), [`paragraphStyles`](https://postext.dev/ar/docs/configuration.md#أنماط-الفقرات), [`unorderedLists`](https://postext.dev/ar/docs/configuration.md#القوائم-غير-المرقّمة)

**واجهات API**

- [`buildDocument`](https://postext.dev/ar/docs/configuration.md#بناء-مستند), [`clearMeasurementCache`](https://postext.dev/ar/docs/configuration.md#ذاكرة-القياس-المؤقتة), [`decompressWoff2`](https://postext.dev/ar/docs/configuration.md#مزوّد-الخطوط-في-المتصفح-fontsource--woff2), [`renderPageToCanvas`](https://postext.dev/ar/docs/configuration.md#رسم-صفحة-في-صورة-نقطية), [`renderToPdf`](https://postext.dev/ar/docs/configuration.md#توليد-ملفات-pdf)

**الخطوط**

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

## طريقة التحضير

### 1 · سمِّ ما يشير إليه النص

الشيفرة هي [الجواب المختصر](#الجواب-المختصر) أعلاه. تأخذ العناوين معرّفًا على طريقة Pandoc، `### Checking pad wear {#sec-pad-wear}`؛ ويبدأ التحذير الخاص بضغط الإطارات بـ `:anchor{#warn-pressure}`، وهي علامة غير مرئية؛ وآخر خطوة في تركيب العجلة هي `[close the lever so that it points backwards]{#step-close-lever}`، التي تحتفظ بكلماتها وتسمّيها؛ ويُفتتح الإطار بـ `:::callout{#box-wet type="note" title="Wet weather"}`. والإحالة إلى الخطوة تطبع كلمات الخطوة، والإحالة إلى الإطار تطبع عنوانه.

### 2 · هدف واحد وأربع طرق لتسميته

يطبع `:ref{id="sec-pad-wear"}` *section 3.2* بالكلمة المأخوذة من `crossRefs.section`؛ ويطبع `style=page` *page 3* بالكلمة المأخوذة من `crossRefs.page`؛ ويطبع `style=title` *Checking pad wear*؛ ويطبع `style=pageNumber` الرقم المجرد *3*. وتجمع قائمة المحتويات على الغلاف الرقم المجرد والعنوان والصفحة المجردة، فتُقرأ كجدول محتويات. وفي الإسبانية تطبع الإحالات نفسها *apartado 3.2* و*página 3*، لأن القوالب تتبع لغة الطبعة.

### 3 · الصفحات تستقر من تلقاء نفسها

رقم الصفحة لا يُعرف إلا بعد إخراج الدليل. يُخرج المحرّك الدليل، ويقرأ الصفحة التي وقع فيها كل هدف، ثم يعيد الإخراج حتى لا تتغير أي إحالة، وهي الحلقة نفسها التي تملأ جدول المحتويات. والنص الذي يطول بالترجمة، أو القسم الذي يُنقل إلى البداية، يغيّر الصفحات، وتتبعها الإحالات، إلى الأمام وإلى الخلف.

### 4 · الأرقام تأتي من عدّاد المستوى الثاني

```js
// script.js, سطرًا 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) };
```

في الدليل عنوان واحد من المستوى الأول، هو اسم الطراز على الغلاف، لذا تُرقَّم أقسامه من المستوى الثاني: يعطي `{2}` من 1 إلى 5، ويعطي `{2}.{3}` 1.1 و1.2.

### 5 · الغلاف هو صفحة افتتاح ذلك العنوان

```js
// script.js, سطرًا 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) },
  ] },
};
```

عمق الشريط 104 مم، نصف الصفحة، ويختمه شريط برتقالي. والعنوان التمهيدي والعنوان الفرعي وسطر الطبعة سمات للعنوان، فتغيّرها الطبعة الإسبانية في نصها الخاص.

```js
// script.js, سطرًا 13–20
const palette = {
  ink: '#1b1f24', navy: '#1d2c3c', orange: '#b8471b', tint: '#f7e7de', rule: '#d5cfc8',
  muted: '#5c636b', paper: '#ffffff',
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
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' } }));
```

## الوصفة كاملة

ملف واحد، مركّب من مجلد الوصفة ومعه نص المثال وأدوات دليل الوصفات المشتركة مضمّنة؛ وهو ينشئ صفحته بنفسه. لتشغيله، ضعه داخل `<script type="module">` في صفحة فارغة أو الصقه في لوحة JS في مثال CodePen جديد (وحدةً). يستورد postext من esm.sh، فلا حاجة إلى تثبيت أو بناء.

- مجلد الوصفة: 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.12.0
import { buildDocument, renderPageToCanvas, clearMeasurementCache } 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',
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
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 = () => ({ // a factory: the engine caches resolved configs per object
  locale: t({ en: 'en-gb', es: 'es' }), // exact codes (gotcha: hyphenation-locales)
  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: [
    // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
    { 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 (gotcha: fonts-first).
const FONTS = {
  Archivo: ['400', '400i', '700'],
  'Archivo Narrow': ['700'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await loadFonts(FONTS, markdown);
const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown);
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 v1 ── the same in every recipe · postext.dev/cookbook ────────
// Postext measures text with the faces the browser has loaded, and caches the
// widths, so every face must be ready before the first build. Faces come from
// Fontsource: the same static files the PDF embeds, so screen and PDF agree.

/** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = {
    latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,'
      + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
    'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,'
      + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF',
  };
  const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin'];
  const jobs = [];
  let added = 0;
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const meta = optional ? await fontsourceMeta(family) : null;
    for (const spec of new Set(specs)) {
      const weight = parseInt(spec, 10);
      const style = spec.endsWith('i') ? 'italic' : 'normal';
      if (hasFace(family, weight, style)) continue;
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`;
        const face = new FontFace(family, `url(${url}) format('woff2')`,
          { weight: String(weight), style, unicodeRange: ranges[subset] });
        jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return added;
}

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

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

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

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

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

/** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */
function fontsourceMeta(family) {
  fontsourceMeta.cache ??= new Map();
  const id = fontsourceId(family);
  if (!fontsourceMeta.cache.has(id)) {
    fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`)
      .then((res) => (res.ok ? res.json() : null), () => null));
  }
  return fontsourceMeta.cache.get(id);
}

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

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

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

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

// ─── Kit · pdf v1 ── the same in every recipe that exports a PDF ──────────────
/** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen
 *  used, snapping to a weight the family ships and falling back to upright
 *  when it has no italic: the PDF asks for every face a block could use. */
async function fontsourceProvider(family, weight, style) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  const weights = meta?.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style;
  const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`);
  if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`);
  return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
}

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

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

## تنويعات

### استعمل «p.» في الإحالات

الكتاب المرجعي الكثير الإحالات إلى الصفحات يوفّر المساحة بالاختصار.

```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}' }),
```

### اترك الإحالات بلون الحبر

حين تكون معظم الصفحة إحالات، أبقها بلون النص وبالأسود العريض؛ فهي تبقى روابط في PDF وعلى الشاشة.

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

## أخطاء شائعة

- **أي كائن headings يُلغي فاصل الصفحة قبل H1.** ينتقل H1 افتراضيًا إلى صفحة فردية (always-odd)، لكن تمرير أي كائن headings يعيد ضبط هذا الافتراض، فتتوالى الفصول دون فاصل ولا يفعل span: 'page' شيئًا. أعد كتابة headings.levels[0].breakBefore: { enabled: true, parity } في كل إعداد.
- **حمّل كل أوجه الخط قبل الإخراج.** يقيس الإخراج النص بأوجه الخط التي حمّلها المتصفح ويخزّن العروض مؤقتًا، فالوجه الذي يصل بعد البناء الأول يترك فواصل أسطر خاطئة وملف PDF لم يعد يطابق الشاشة. حمّل كل وزن وكل نمط أولًا، واستدعِ clearMeasurementCache() قبل إعادة البناء إذا تأخر وصول أحدها.
- **لوحة الألوان المستبدلة لا تصل إلى عناصر التصميم ولا إلى لون الإحالة.** يقرأ postext 1.4.1 الإعداد colorPalette في أنماط النص (المتن والعناوين والقوائم والتعليقات والجداول والإطارات) لكن لا في عناصر الترويسات والتذييلات والافتتاحيات وصفحات الأجزاء، ولا في bodyText.referenceColor: تحتفظ بالقيمة الست عشرية المكتوبة بجانب paletteId الخاص بها. حين تستبدل لوحة الألوان، لنسخة شاشة داكنة أو لإعادة تلوين، أعِد كتابة كل لون مرتبط من colorPalette قبل البناء.
- **8 لغات فقط تُقسَّم بالواصلة، بالرمز المطابق تمامًا.** يتوفر تقسيم الكلمات بالواصلة للغات en-us وes وfr وde وit وpt وca وnl، بمطابقة تامة للرمز: 'es-ES' أو أي لغة أخرى تعود دون تنبيه إلى الإنجليزية الأمريكية.
- **القوائم تقول 'arabic'، والموارد 'roman-upper'، والصفحات 'upper-roman'.** كل إعداد ترقيم يكتب صيغه بطريقة مختلفة: القوائم تأخذ numberFormat 'arabic' (القيمة 'decimal' تطبع "undefined")، وأنواع الموارد تأخذ counterFormat 'roman-upper'، والصفحات و:::numbering تأخذ 'upper-roman'.
- **يُخزَّن الإعداد مؤقتًا بحسب هويته: ابنِ كائنًا جديدًا.** يخزّن المحرّك الإعدادات المحسوبة مؤقتًا بحسب هوية الكائن، فتعديل الإعداد في مكانه ثم البناء مجددًا يعيد استخدام النتيجة القديمة. ابنِ كائنًا جديدًا في كل بناء، ولهذا يكون إعداد الوصفة دالة مصنِّعة: config().

- الإحالة إلى `:anchor` بلا نمط تطبع الصفحة، لأن المرساة غير المرئية لا كلمات لها. استعمل `[words]{#id}` حين يجب أن تقتبس الإحالة النص.
- يجب أن يكون المعرّف فريدًا في الكتاب. تصل الإحالات إلى أول `{#sec-brakes}`، وتُدرج لوحة الفحوص في Sandbox المعرّفَ الثاني بوصفه `duplicateAnchor`.

## الحقوق

- الوصفة: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- الخطوط: Archivo (OFL-1.1), Archivo Narrow (OFL-1.1)
- الشيفرة: MIT · محتوى المثال: CC-BY-4.0

## وصفات ذات صلة

- [رقم 094 · كتاب تقني تعبر إحالاته بين الفصول](https://postext.dev/ar/cookbook/technical-book-crossref-chapters.md): دليل طاقة شمسية من ثلاثة فصول يكتب نصه @fig:sun و@tbl:loads و@sec:array-size، وتطبع buildBundle كل رقم وصفحة عبر الفصول. · المستوى 3 (متقدم) · الأدلة والمراجع, الكتب المدرسية
- [رقم 007 · كتاب واحد من فصول منفصلة](https://postext.dev/ar/cookbook/book-from-chapters.md): ينضّد buildBundle خمسة ملفات Markdown كتابًا واحدًا: يبدأ كل فصل على صفحة فردية، وتتواصل أرقام الصفحات والفصول والأشكال من ملف إلى آخر. · المستوى 3 (متقدم) · الأدلة والمراجع
- [رقم 019 · أجزاء ملوّنة من سمة واحدة](https://postext.dev/ar/cookbook/parts-in-colour.md): دليل ميداني للجيب يضبط فيه كل :::part مدخلًا من لوحة الألوان، فتُطبع بلونه صفحته الفاصلة وظهرها المطلي وألسنة الإبهام والعلامات الميدانية وسطر المحتويات. · المستوى 3 (متقدم) · الأدلة والمراجع
