# Farbcodierte Hervorhebungen in einem Lesebuch

> Ein Geschichtslesebuch, dessen Schlüsselwörter drei Farben tragen (grün für Begriffe, bernstein für Daten, blau für Namen), aus drei markStyles der Palette.

- HTML-Fassung: https://postext.dev/de/cookbook/colour-coded-study-highlights
- Rezept Nº 180 · Schrift und Text · Stufe 2 (Mittel) · Ausgaben: Canvas
- Gattungen: Lehrbücher
- Benötigt postext ≥ 1.65.0 · getestet mit 1.65.0 am 2026-10-31
- Seiten: [1](https://postext.dev/cookbook/colour-coded-study-highlights/en/p01.webp?v=d1c18f3b), [2](https://postext.dev/cookbook/colour-coded-study-highlights/en/p02.webp?v=d1c18f3b), [3](https://postext.dev/cookbook/colour-coded-study-highlights/en/p03.webp?v=d1c18f3b), [4](https://postext.dev/cookbook/colour-coded-study-highlights/en/p04.webp?v=d1c18f3b)
- In der Sandbox öffnen: https://postext.dev/de/sandbox#recipe=colour-coded-study-highlights&lang=en (.postext: https://postext.dev/cookbook/colour-coded-study-highlights/en/colour-coded-study-highlights.postext)
- Zuletzt aktualisiert: 2026-10-31
- Andere Sprachen: [en](https://postext.dev/en/cookbook/colour-coded-study-highlights.md), [es](https://postext.dev/es/cookbook/colour-coded-study-highlights.md), [ca](https://postext.dev/ca/cookbook/colour-coded-study-highlights.md), [fr](https://postext.dev/fr/cookbook/colour-coded-study-highlights.md), [it](https://postext.dev/it/cookbook/colour-coded-study-highlights.md), [pt](https://postext.dev/pt/cookbook/colour-coded-study-highlights.md), [zh](https://postext.dev/zh/cookbook/colour-coded-study-highlights.md), [ja](https://postext.dev/ja/cookbook/colour-coded-study-highlights.md), [ar](https://postext.dev/ar/cookbook/colour-coded-study-highlights.md)

## Kurz gesagt

Ein Lesetext für die Schule über die Geschichte des Buchdrucks. Fachbegriffe sind grün markiert, Jahreszahlen bernsteinfarben, Personen und Orte blau, und eine Legende auf der ersten Seite sagt, was jede Farbe bedeutet.

## Was Sie setzen

Lesetext 2 einer Unterrichtseinheit in einem Geschichtslesebuch, *Readings in Early Modern History*: vier Seiten darüber, wie sich der Buchdruck zwischen 1450 und 1550 von Mainz aus über Europa verbreitete, auf einer Seite von 170 × 240 mm. Die Herausgeber haben den Text markiert, wie es eine sorgfältige Schülerin täte, mit einem Code, der im ganzen Buch gleich bleibt: Fachbegriffe auf einem grünen Band, Jahreszahlen auf Bernstein, Personen und Orte auf Hellblau. Eine Legende unter dem Titel erklärt den Code mit den drei Farben selbst, und das Glossar auf der letzten Seite beginnt jeden Eintrag mit seinem grünen Begriff. Die Markierungen sind Bänder unter gewöhnlichem Text, also brechen, trennen und füllen sich die Zeilen, als gäbe es die Bänder nicht.

**Dieses Rezept beantwortet:**

- Wie hebe ich einen Lerntext in Farben hervor, die jeweils etwas bedeuten: Begriffe, Daten und Namen?

## Die kurze Antwort

```js
// script.js, Zeilen 37–52
// :mark[Mainz]{style="name"} in the text picks a style by id. The band is painted under
// words that stay ordinary text, so the lines break, hyphenate and justify as if the
// mark were not there, and a mark that runs over a line end gets a band on each line.
const band = (id, name) => ({
  id, name,
  background: col(id), // pale enough for ink at full contrast on top
  color: col('ink'), // the words keep the colour of the text
  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
  paddingX: em(0.12), // the band reaches a little past the first and the last letter
  paddingTop: em(0.04), paddingBottom: em(0.1), // and down past the descenders
});
const markStyles = [
  band('term', t({ en: 'Key term', es: 'Término clave' })),
  band('date', t({ en: 'Date', es: 'Fecha' })),
  band('name', t({ en: 'Name', es: 'Nombre' })),
];
```

## Zutaten

**Zeigt**

- [Hervorhebungen](https://postext.dev/de/docs/document-format-inline.md#hervorhebungen): Ein farbiges Band hinter einer Wendung, wie ein Schulbuch seine Schlüsselwörter hervorhebt: Die Wörter bleiben gewöhnlicher Text und brechen um, das Band folgt jedem Stück.

**Verwendet außerdem**

- [Kästen](https://postext.dev/de/docs/configuration-callouts.md#kastenstile)
- [Semantische Farbpalette](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette)
- [Gestaltete Auftakte](https://postext.dev/de/docs/configuration-headings.md#seitenbreite-und-erweiterte-gestaltung)
- [Bilder in Seitengestaltungen](https://postext.dev/de/docs/configuration-headers-footers.md#bildelemente)
- [Überschriftenattribute](https://postext.dev/de/docs/document-format.md#attribute-von-überschriften)
- [Kolumnentitel und Seitenzahlen](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen)
- [Kopfzeilen nach Seitenrolle](https://postext.dev/de/docs/configuration-headers-footers.md#textelemente)
- [Gespiegelte Ränder](https://postext.dev/de/docs/configuration-page-layout.md#gespiegelte-ränder)
- [Absatzformate](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Witwen, Waisen und kurze Ausgangszeilen](https://postext.dev/de/docs/configuration-page-flow.md#waisen-witwen-zu-kurze-ausgangszeilen-und-regeln-zum-zusammenhalten)
- [Literaturverzeichnisse und Glossare](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Abbildung und Tabelle in Ihrer Sprache](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen)
- [Eigene Ressourcentypen](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen)
- [Abbildungen und Tabellen als Ressourcen](https://postext.dev/de/docs/document-format-resources.md#ressourcen)

**Die Konfiguration auf einen Blick**

- [`bodyText`](https://postext.dev/de/docs/configuration-text.md#fließtext), [`calloutStyles`](https://postext.dev/de/docs/configuration-callouts.md#kastenstile), [`captionStyle`](https://postext.dev/de/docs/configuration-resources.md#legendenstil), [`colorPalette`](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette), [`footer`](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen), [`header`](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen), [`headings`](https://postext.dev/de/docs/configuration-headings.md#überschriften), [`layout`](https://postext.dev/de/docs/configuration-page-layout.md#layout), [`locale`](https://postext.dev/de/docs/configuration-language.md#silbentrennung), [`markStyles`](https://postext.dev/de/docs/configuration-styles.md#hervorhebungsstile), [`orderedLists`](https://postext.dev/de/docs/configuration-lists-math.md#nummerierte-listen), [`page`](https://postext.dev/de/docs/configuration-page-layout.md#seite), [`paragraphStyles`](https://postext.dev/de/docs/configuration-styles.md#absatzformate), [`resourceTypes`](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen), [`unorderedLists`](https://postext.dev/de/docs/configuration-lists-math.md#aufzählungen)

**APIs**

- [`buildDocumentWithFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden), [`defaultResourceTypes`](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen), [`registerResourceImage`](https://postext.dev/de/docs/architecture.md#api-oberfläche), [`renderPageToCanvas`](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern)

**Schriften**

- Source Serif 4 (OFL-1.1), Source Sans 3 (OFL-1.1)

## Zubereitung

### 1 · Ein Stil für jede Art von Wort

Der Code steht in [der kurzen Antwort](https://postext.dev/de/cookbook/colour-coded-study-highlights.md#die-kurze-antwort) oben. `markStyles` deklariert drei benannte Stile, und der Text wählt für jede Markierung einen: `:mark[Johann Fust]{style="name"}`, `:mark[1455]{style="date"}`, `:mark[colophon]{style="term"}` ([Hervorhebungen](https://postext.dev/de/docs/document-format-inline.md#hervorhebungen)). Jeder Stil sagt, was er bedeutet (`name` sieht, wer den Text bearbeitet), also hält das Markdown fest, warum ein Wort markiert ist, nicht welche Farbe es trägt. Ändern Sie die Farbe eines Stils, ändern sich alle Jahreszahlen des Buchs mit. Die Enden sind gerade, wie seit postext 1.52 voreingestellt ([Hervorhebungsstile](https://postext.dev/de/docs/configuration-styles.md#hervorhebungsstile)): Ein Textmarker hinterlässt gerade Enden, ein abgerundetes Band liest sich wie ein Chip.

### 2 · Drei Töne aus der Palette

```js
// script.js, Zeilen 14–29
const palette = {
  ink: '#1f1c19', // text: a warm near-black
  accent: '#8a3220', // kickers, rules, numbers: an iron-gall red
  tint: '#f5f0e7', // the key's background
  rule: '#d3cabc', // hairlines
  muted: '#6b645b', // running heads, colophon
  paper: '#ffffff',
  term: '#cce8c8', // key terms: a pale green
  date: '#fbe0a1', // dates: amber
  name: '#cfe0f4', // people and places: a pale blue
};
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.accent, model: 'hex' } },
];
```

Die drei Bänder sind Einträge der Palette wie die Schrift- und die Akzentfarbe, also kann, wer das Buch im Sandbox öffnet, den Code an einem einzigen Farbfeld umfärben. Sie sind hell, etwa so hell wie der Fond des Kastens, und die Schrift darauf behält einen Kontrast von 12,6:1 oder mehr; die drei bleiben unterscheidbar, wo sie in einer Zeile aufeinandertreffen, wie bei „In 1517 Martin Luther“ auf Seite 3. Eine Markierung braucht keinen Platz: `paddingX` reicht 0,12 em in den Wortzwischenraum, `paddingBottom` zieht das Band unter die Unterlängen, beides in Raum, der schon da war.

### 3 · Der Auftakt: ein Gemälde über dem Titel

```js
// script.js, Zeilen 56–81
const opener = {
  enabled: true,
  minHeight: mm(122), // the floor of the opener: the key starts on the same line
  marginBottom: pt(0),
  slot: {
    elements: [
      { kind: 'image', id: 'press', resourceId: 'press', fit: 'cover',
        placement: { anchor: { to: 'page', edge: 'top-left' },
          size: { width: mm(170), height: mm(96) } } },
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: SANS,
        fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
        textTransform: 'uppercase', color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(82) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF,
        fontSize: pt(26), lineHeight: 1.08, fontWeight: 700, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(2) },
          size: { width: mm(126), height: 'auto' } } },
      { kind: 'text', id: 'standfirst', content: '{attr.standfirst}', fontFamily: SERIF,
        italic: true, fontSize: pt(10.5), lineHeight: LEAD / 10.5, color: col('ink'),
        align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(118), height: 'auto' } } },
    ],
  },
};
```

Die Überschrift erster Ebene druckt ein Design. Das Gemälde ist an der Seite verankert und läuft oben und an beiden Seiten in den Anschnitt, 170 mal 96 mm, mit `fit: 'cover'`. Dachzeile und Vorspann kommen aus Attributen der Überschriftenzeile ([Attribute von Überschriften](https://postext.dev/de/docs/document-format.md#attribute-von-überschriften)), und die Legende folgt als gewöhnlicher `:::callout`, dessen Listenpunkte mit den drei Markierungen beginnen. `minHeight` legt die Tiefe fest: Die Legende beginnt auf derselben Zeile, wie lang der Titel auch ist.

### 4 · Kolumnentitel nach Seitenparität

```js
// script.js, Zeilen 85–106
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: SANS, fontSize: pt(7.5), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(13) } },
  ...extra,
});
const folio = { fontWeight: 700, color: col('ink') };
const header = {
  elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
    head('verso-title', '{title}', 'even', 'top-left', OUTER + 9),
    head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
  ],
};
const footer = {
  elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
    ...folio, pages: 'opener',
    placement: { anchor: { to: 'container', edge: 'top' }, offset: { x: mm(0), y: mm(9) } },
  })],
};
```

Die linke Seite trägt den Titel der Reihe, die rechte den des Lesetexts, beide gefiltert mit `parity` und durch `pages: 'body'` vom Auftakt ferngehalten ([Textelemente](https://postext.dev/de/docs/configuration-headers-footers.md#textelemente)). Der Auftakt trägt seine Seitenzahl am Fuß.

## Das ganze Rezept

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

- Quellordner: https://github.com/drnachio/postext/tree/main/cookbook/colour-coded-study-highlights

### script.js

```js
// ═══ Postext Cookbook · Nº 180 · Colour-coded highlights in a study reader ════════
// https://postext.dev/en/cookbook/colour-coded-study-highlights
// Code: MIT · Text: original (CC BY 4.0) · Picture: painting made for the recipe
// Fonts: Source Serif 4, Source Sans 3 (SIL OFL 1.1) · Needs postext ≥ 1.65.0
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, defaultResourceTypes,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'colour-coded-study-highlights';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: semantic colours, three of them for the highlighter
const palette = {
  ink: '#1f1c19', // text: a warm near-black
  accent: '#8a3220', // kickers, rules, numbers: an iron-gall red
  tint: '#f5f0e7', // the key's background
  rule: '#d3cabc', // hairlines
  muted: '#6b645b', // running heads, colophon
  paper: '#ffffff',
  term: '#cce8c8', // key terms: a pale green
  date: '#fbe0a1', // dates: amber
  name: '#cfe0f4', // people and places: a pale blue
};
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.accent, model: 'hex' } },
];
// #endregion
const OUTER = 20; // outer margin in mm: the running heads align to it
const LEAD = 14.5; // body leading in pt
const SANS = 'Source Sans 3';
const SERIF = 'Source Serif 4';

// #region answer: three highlight styles, one per kind of word, linked to the palette
// :mark[Mainz]{style="name"} in the text picks a style by id. The band is painted under
// words that stay ordinary text, so the lines break, hyphenate and justify as if the
// mark were not there, and a mark that runs over a line end gets a band on each line.
const band = (id, name) => ({
  id, name,
  background: col(id), // pale enough for ink at full contrast on top
  color: col('ink'), // the words keep the colour of the text
  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
  paddingX: em(0.12), // the band reaches a little past the first and the last letter
  paddingTop: em(0.04), paddingBottom: em(0.1), // and down past the descenders
});
const markStyles = [
  band('term', t({ en: 'Key term', es: 'Término clave' })),
  band('date', t({ en: 'Date', es: 'Fecha' })),
  band('name', t({ en: 'Name', es: 'Nombre' })),
];
// #endregion

// #region opener: the painting bleeds off the top of the page, the title sits under it
const opener = {
  enabled: true,
  minHeight: mm(122), // the floor of the opener: the key starts on the same line
  marginBottom: pt(0),
  slot: {
    elements: [
      { kind: 'image', id: 'press', resourceId: 'press', fit: 'cover',
        placement: { anchor: { to: 'page', edge: 'top-left' },
          size: { width: mm(170), height: mm(96) } } },
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: SANS,
        fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
        textTransform: 'uppercase', color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(82) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF,
        fontSize: pt(26), lineHeight: 1.08, fontWeight: 700, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(2) },
          size: { width: mm(126), height: 'auto' } } },
      { kind: 'text', id: 'standfirst', content: '{attr.standfirst}', fontFamily: SERIF,
        italic: true, fontSize: pt(10.5), lineHeight: LEAD / 10.5, color: col('ink'),
        align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(118), height: 'auto' } } },
    ],
  },
};
// #endregion

// #region running-heads: series title on the verso, reading title on the recto
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: SANS, fontSize: pt(7.5), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(13) } },
  ...extra,
});
const folio = { fontWeight: 700, color: col('ink') };
const header = {
  elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
    head('verso-title', '{title}', 'even', 'top-left', OUTER + 9),
    head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
  ],
};
const footer = {
  elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
    ...folio, pages: 'opener',
    placement: { anchor: { to: 'container', edge: 'top' }, offset: { x: mm(0), y: mm(9) } },
  })],
};
// #endregion

const config = {
  locale: t({ en: 'en-us', es: 'es' }),
  resourceTypes: defaultResourceTypes(LANG),
  colorPalette,
  markStyles,
  page: {
    sizePreset: 'custom', width: mm(170), height: mm(240), dpi: 150,
    margins: { top: mm(22), bottom: mm(22), left: mm(22), right: mm(OUTER), mirror: true },
  },
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: SERIF, fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false,
    hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: {
    fontFamily: SERIF, color: col('ink'), fontWeight: 700,
    levels: [
      { level: 1, fontSize: pt(26), breakBefore: { parity: 'odd' }, advancedDesign: opener },
      { level: 2, fontFamily: SANS, fontSize: pt(12), lineHeight: pt(LEAD), fontWeight: 700,
        color: col('accent'), marginTop: pt(LEAD), marginBottom: pt(0) },
    ],
  },
  unorderedLists: { bulletChar: '–', color: col('accent'), fontWeight: 400,
    marginTop: pt(0), marginBottom: pt(0) },
  orderedLists: { numberFormat: 'arabic', color: col('accent'), fontWeight: 700,
    marginTop: pt(0), marginBottom: pt(0) },
  paragraphStyles: [
    // The glossary: each green term opens its entry, the turnovers hang 1.5 em.
    { id: 'glossary', fontSize: pt(9.5), lineHeight: pt(13), textAlign: 'left',
      hangingIndent: em(1.5) },
    { id: 'colophon', fontFamily: SANS, fontSize: pt(7.5), lineHeight: pt(10),
      color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
  ],
  // The key: a tint with a top stripe, its title in the label face.
  calloutStyles: [
    { id: 'key', title: t({ en: 'Key', es: 'Clave' }), background: col('tint'),
      stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('accent') },
      padding: { top: mm(3), right: mm(4), bottom: mm(3), left: mm(4) },
      titleStyle: { fontFamily: SANS, fontSize: pt(8), fontWeight: 700,
        textTransform: 'uppercase', letterSpacing: pt(1.2), color: col('accent') },
      body: { fontFamily: SANS, fontSize: pt(9.5), lineHeight: pt(13), color: col('ink'),
        firstLineIndent: pt(0), textAlign: 'left', boldColor: col('ink'),
        italicColor: col('ink') } },
  ],
  captionStyle: { fontFamily: SANS, fontSize: pt(8), color: col('ink'), labelBold: true,
    labelColor: col('accent'), note: { color: col('muted') } },
  header,
  footer,
};

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Readings in Early Modern History"
author: "Study edition"
---

# How Printing Spread Across Europe {kicker="Unit 4 · Reading 2" standfirst="In a hundred years a craft from one German town filled Europe with books. This reading follows the press from Mainz to more than two hundred towns and asks what changed when books became cheap."}

:::callout{type="key" title="Reading the highlights"}
The editors have marked three kinds of words in this reading. Use the same colours in your own notes.

- :mark[Key term]{style="term"}: a word the glossary at the end of the unit defines.
- :mark[Date]{style="date"}: a year or a period to place on your timeline.
- :mark[Name]{style="name"}: a person or a place in the story.
:::

## A workshop in Mainz

Around :mark[1450]{style="date"} a goldsmith named :mark[Johannes Gutenberg]{style="name"} was running a printing workshop in :mark[Mainz]{style="name"}, a city on the Rhine. Printing itself was not new. Printers in China had cut whole pages into wooden blocks for centuries, and Korean printers had cast metal type by the :mark[thirteenth century]{style="date"}. What Gutenberg put together was a system that suited the twenty-odd letters of the Latin alphabet.

The system had four parts. The first was :mark[movable type]{style="term"}: single letters cast in metal, set in lines, printed and then put back in the case for the next page. A hand mould cast thousands of identical letters from an alloy of lead, tin and antimony. An ink made with linseed oil clung to the metal where a watery ink would have run off. And a :mark[screw press]{style="term"}, of the kind used to press grapes and to flatten wet paper, pushed the paper evenly onto the inked type.

The first large book from the workshop was a Latin Bible in two volumes, set in two columns of 42 lines, finished by :mark[1455]{style="date"}. About 180 copies were printed, most on paper and some on vellum. A single scribe could spend years copying one Bible; the workshop produced its whole edition in about the same time.

Gutenberg had borrowed heavily to build all this. His lender, the Mainz merchant :mark[Johann Fust]{style="name"}, took him to court in :mark[1455]{style="date"} and kept much of the equipment. Fust carried on the business with :mark[Peter Schöffer]{style="name"}, who had trained in the workshop. Their Psalter of :mark[1457]{style="date"} is the first printed book whose :mark[colophon]{style="term"}, the closing note of a book, names its printers and the day it was finished.

## The craft travels

For about ten years printing stayed close to Mainz. In :mark[1462]{style="date"} the city was sacked in a war between two rival archbishops, and many of its craftsmen left. Printers trained on the Rhine set up wherever they found readers, paper and money: at :mark[Subiaco]{style="name"}, near Rome, in :mark[1465]{style="date"}; in :mark[Venice]{style="name"} in :mark[1469]{style="date"}; in :mark[Paris]{style="name"}, in rooms at the Sorbonne, in :mark[1470]{style="date"}. A press was at work in :mark[Segovia]{style="name"} around :mark[1472]{style="date"}, and :mark[Valencia]{style="name"} had one by :mark[1474]{style="date"}.

:mark[William Caxton]{style="name"}, an English merchant who had learned the craft in :mark[Cologne]{style="name"}, set up his press at :mark[Westminster]{style="name"} in :mark[1476]{style="date"}. Most printers of the time worked in Latin, the language of the Church and the universities, and sold across borders. Caxton printed mostly in the :mark[vernacular]{style="term"}, the language people spoke at home: romances, histories and Chaucer's *Canterbury Tales*, for readers who had no Latin.

Books printed before :mark[1501]{style="date"} are called :mark[incunabula]{style="term"}, from the Latin word for a cradle: the books of printing's infancy. Catalogues today list about 30,000 editions from those fifty years. By :mark[1500]{style="date"} presses had worked in more than 250 towns, and estimates of the copies they printed run from eight to twenty million.

## Venice and the pocket book

By the end of the century :mark[Venice]{style="name"} had more printing houses than any other city in Europe. It had paper mills on the mainland, ships to carry books to every port of the Mediterranean, and buyers with money. :mark[Aldus Manutius]{style="name"} opened his press there in the :mark[mid-1490s]{style="date"} and printed the Greek classics in editions prepared by scholars. His books carried a printer's mark, a dolphin wrapped round an anchor, so that buyers could tell them from the copies other printers soon made.

In :mark[1501]{style="date"} Aldus issued a Virgil in :mark[octavo]{style="term"}, a sheet folded three times to make sixteen pages, so that the book fitted in a hand. It was set in a slanting letter cut by :mark[Francesco Griffo]{style="name"}, which we now call :mark[italic]{style="term"}. The narrow letters fitted more words on each line, so the small books needed less paper and cost less. Readers could carry a poet in a pocket instead of reading him at a desk.

## Print and the Reformation

In :mark[1517]{style="date"} :mark[Martin Luther]{style="name"}, who taught theology at :mark[Wittenberg]{style="name"}, wrote ninety-five theses against the sale of indulgences. Printers in several German cities reprinted them within weeks. Luther went on to write short :mark[pamphlets]{style="term"} in German: a few pages each, quick to set, cheap to buy and easy to pass from hand to hand. Historians estimate that between :mark[1518]{style="date"} and :mark[1525]{style="date"} his writings made up about a third of all the books printed in German.

His opponents answered in print too, and the quarrel over the Church was argued in thousands of pamphlets read aloud in taverns and market squares. Printing did not cause the Reformation, but it carried each side's arguments further and faster than any preacher could.

## Check your reading

1. Name the four parts of Gutenberg's system, and say what each one did.
2. Why did the sack of Mainz in 1462 help printing to spread?
3. What is a colophon, and why would a historian be glad to find one?
4. Caxton chose the vernacular. Who were his readers, and why did that choice sell books?
5. Put the amber dates of this reading on a timeline from 1450 to 1525.

## Glossary

:::paragraphs{style="glossary"}
**:mark[colophon]{style="term"}** A note at the end of a book that gives the printer, the place and the date of printing.

**:mark[incunabula]{style="term"}** Books printed in Europe before 1501. One such book is an incunable.

**:mark[italic]{style="term"}** A slanting style of type, first cut by Francesco Griffo for Aldus Manutius.

**:mark[movable type]{style="term"}** Single letters cast one by one, which can be set in lines, printed and used again.

**:mark[octavo]{style="term"}** A book whose sheets are folded three times, giving sixteen pages to the sheet.

**:mark[pamphlet]{style="term"}** A short booklet of a few unbound pages, quick and cheap to print.

**:mark[screw press]{style="term"}** A press in which a turning screw drives a flat plate down onto the paper and the type.

**:mark[vernacular]{style="term"}** The language people speak at home, as opposed to Latin.
:::

:::paragraphs{style="colophon"}
Set in Source Serif 4 and Source Sans 3 (SIL Open Font License) · Text: original, CC BY 4.0 · Opening picture: a painting made for this edition.
:::
`; // content.<lang>.md: the reading, its key and questions

// #region art: a printing workshop around 1460, a painting in assets/ (no lettering)
const resources = [{ id: 'press', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'press-1400.jpg', format: 'jpeg', width: 1400, height: 790 },
  altText: t({
    en: 'A printing workshop: a compositor picks type from a case by the window while a '
      + 'pressman pulls the bar of a wooden screw press; printed sheets hang drying overhead.',
    es: 'Un taller de imprenta: un cajista toma tipos de la caja junto a la ventana mientras '
      + 'un prensista tira de la barra de una prensa de husillo; arriba se secan los pliegos.',
  }) }];
await loadImage(resources[0].bitmap.fileId, asset(resources[0].bitmap.fileId));
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Source Serif 4': ['400', '400i', '700'],
  'Source Sans 3': ['400', '600', '700'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const doc = await buildDocumentWithFonts({ markdown, resources }, config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'A study reader in three highlight colours',
  es: 'Un libro de lecturas resaltado en tres colores' }) });

// ─── 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 and asks the kit
// for Fontsource's files, the ones the PDF embeds too.

/** buildDocumentWithFonts' options: the resolver, and FONTS' faces loaded
 *  with the config's. 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 files with their ranges: latin, then latin-ext and greek when
 *  `text` needs them (kitSubsetsFor). Null for a face Fontsource lacks. */
async function fontsourceResolver(family, weight, style, { text = '' } = {}) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  if (meta && !(await fontsourceWeights(family, style)).includes(weight)) 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],
  }));
}

/** Each file's unicode-range (a function: the kit runs after the pen's code). */
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: loads `faces` (as FONTS) for the
 *  sample `text`; `optional` skips a face Fontsource lacks. Files are added in
 *  the order asked, the last winning a shared character. Resolves to the count. */
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 && (await fontsourceWeights(family, style)).includes(weight))) 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 face (fonts.check() says yes to any family). */
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, id = fontsourceId(family)) {
  return (fontsourceMeta.c ??= {})[id] ??= fetch(`https://api.fontsource.org/v1/fonts/${id}`)
    .then((res) => (res.ok ? res.json() : null), () => null);
}

/** The weights the package ships in `style`: the API pairs none (no Libre
 *  Caslon 700 italic file), so italics are read from latin-italic.css. */
function fontsourceWeights(family, style, id = fontsourceId(family)) {
  return (fontsourceWeights.c ??= {})[id + style] ??= fontsourceMeta(family).then(async (m) => {
    if (!m?.styles.includes(style)) return m ? [] : [400, 700];
    const css = style === 'italic' ? await fetch(`${await fontsourceAt(id)}latin-italic.css`).then((r) => (r.ok ? r.text() : ''), () => '') : '';
    const w = [...css.matchAll(/weight: (\d+)/g)].map((x) => +x[1]);
    return w.length ? w : m.weights;
  });
}

// ─── 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 · images v3 ── recipes with pictures · postext.dev/cookbook
/** Registers a photo or PNG for the canvas and keeps its bytes for the PDF.
 *  fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */
async function loadImage(fileId, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`);
  const bytes = new Uint8Array(await res.arrayBuffer());
  registerResourceImage(fileId, await createImageBitmap(new Blob([bytes])));
  (loadImage.bytes ??= new Map()).set(fileId, bytes);
}

/** Registers SVG markup as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25).
 *  `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */
async function loadSvg(fileId, svg, options) {
  if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces);
  const img = new Image();
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await img.decode();
  registerResourceImage(fileId, img, options);
  (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg));
}

/** A face's Fontsource files: nearest weight; latin + what the SVG sets. */
async function svgFaces(family, weight, style, { codePoints = [] } = {}) {
  const [id, m] = [fontsourceId(family), await fontsourceMeta(family)];
  const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight;
  const s = m?.styles.includes(style) === false ? 'normal' : style;
  return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array(
    await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer())));
}

/** renderToPdf({ resourceBytes: imageBytes }) */
function imageBytes(fileId) { return loadImage.bytes?.get(fileId); }

/** renderToHtml({ resourceImageUrl: imageUrl }) */
function imageUrl(fileId) {
  const bytes = imageBytes(fileId);
  if (!bytes) return undefined;
  imageUrl.urls ??= new Map();
  if (!imageUrl.urls.has(fileId)) {
    const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg';
    imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type })));
  }
  return imageUrl.urls.get(fileId);
}

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

## Varianten

### Die Enden der Bänder runden

Ein Radius von einem Fünftel des Schriftgrads gibt die weichen Enden eines Filzstifts.

```diff
-  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
+  borderRadius: em(0.2), // soft ends, as a felt-tip pen leaves them
```

### Jeder Zeile ein eigenes Band geben

Mit `decorationBreak: 'clone'` bekommt eine Markierung, die umbricht, an beiden Enden jedes Stücks ihren Überstand, wie auf Seite 3, wo „pamphlets“ nach „pam-“ getrennt wird.

```diff
   paddingX: em(0.12), // the band reaches a little past the first and the last letter
+  decorationBreak: 'clone', // each line piece gets a whole band
```

## Häufige Fehler

- Farbe allein übersteht keine Schwarzweißkopie, und so kommen viele Lesetexte ins Klassenzimmer. Das Glossar setzt die Begriffe zusätzlich fett; wenn Ihre Leser die Seiten kopieren, geben Sie einer der drei Arten ein zweites, eigenes Zeichen.
- Eine Stil-ID, die `markStyles` nicht deklariert, nimmt den ersten Stil, ohne Fehler auf der Seite (das Sandbox meldet sie). Halten Sie die IDs kurz und prüfen Sie ihre Schreibung im Text.

## Nachweise

- Rezept: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Text: The reading, its key, the review questions and the colophon: Ignacio Ferro, CC-BY-4.0
- Bilder: The printing workshop on the opening page, a painting: Generated With Diffusion Models, Original
- Schrift: Source Serif 4 (OFL-1.1), Source Sans 3 (OFL-1.1)
- Code: MIT · Beispielinhalt: CC-BY-4.0

## Verwandte Rezepte

- [Nº 076 · Eine Pinyin-Fibel: Lesungen über jedem Zeichen](https://postext.dev/de/cookbook/pinyin-primer.md): Mono-Ruby in einer Hongkonger Fibel: {人之初|rén zhī chū} zentriert eine Pinyin-Silbe über jedem Zeichen, in der Andika, in einem Zeilenzwischenraum von 28 pt. · Stufe 2 (Mittel) · Lehrbücher, Arbeitshefte und Übungen
- [Nº 163 · Ein Harmonielehrbuch mit Notenbeispielen in ABC](https://postext.dev/de/cookbook/harmony-textbook-abc-examples.md): Ein Kapitel über Kadenzen, dessen Notenbeispiele ABC-Blöcke im Markdown sind, auf Spaltenbreite gestochen; eines davon läuft über zwei Seiten. · Stufe 2 (Mittel) · Lehrbücher
- [Nº 122 · Kanbun mit Lesezeichen und japanischer Lesung](https://postext.dev/de/cookbook/kanbun-kundoku.md): Drei Analektensprüche und ein Tang-Vierzeiler im Satz eines japanischen Lesebuchs: Rückkehrzeichen und Okurigana in Zinnober, darunter die Lesung in Kana. · Stufe 2 (Mittel) · Lehrbücher
