# Ein Klaviersatz aus MusicXML über mehrere Seiten

> Zwei Menuette für Tasteninstrument, aus MusicXML-Dateien im Klaviersystem gestochen und zwischen Systemen geteilt, sodass jedes Stück eine Doppelseite füllt.

- HTML-Fassung: https://postext.dev/de/cookbook/piano-score-from-musicxml
- Rezept Nº 167 · Abbildungen und Bilder · Stufe 2 (Mittel) · Ausgaben: Canvas, PDF
- Gattungen: Jedes Genre
- Benötigt postext ≥ 1.54.0, postext-pdf ≥ 1.54.0 · getestet mit 1.54.0, postext-pdf 1.54.0 am 2026-10-22
- Seiten: [1](https://postext.dev/cookbook/piano-score-from-musicxml/en/p01.webp?v=c38bdad1), [2](https://postext.dev/cookbook/piano-score-from-musicxml/en/p02.webp?v=c38bdad1), [3](https://postext.dev/cookbook/piano-score-from-musicxml/en/p03.webp?v=c38bdad1), [4](https://postext.dev/cookbook/piano-score-from-musicxml/en/p04.webp?v=c38bdad1), [5](https://postext.dev/cookbook/piano-score-from-musicxml/en/p05.webp?v=c38bdad1)
- PDF: https://postext.dev/cookbook/piano-score-from-musicxml/en/piano-score-from-musicxml.pdf?v=c38bdad1
- In der Sandbox öffnen: https://postext.dev/de/sandbox#recipe=piano-score-from-musicxml&lang=en (.postext: https://postext.dev/cookbook/piano-score-from-musicxml/en/piano-score-from-musicxml.postext)
- Zuletzt aktualisiert: 2026-10-22
- Andere Sprachen: [en](https://postext.dev/en/cookbook/piano-score-from-musicxml.md), [es](https://postext.dev/es/cookbook/piano-score-from-musicxml.md), [ca](https://postext.dev/ca/cookbook/piano-score-from-musicxml.md), [fr](https://postext.dev/fr/cookbook/piano-score-from-musicxml.md), [it](https://postext.dev/it/cookbook/piano-score-from-musicxml.md), [pt](https://postext.dev/pt/cookbook/piano-score-from-musicxml.md), [zh](https://postext.dev/zh/cookbook/piano-score-from-musicxml.md), [ja](https://postext.dev/ja/cookbook/piano-score-from-musicxml.md), [ar](https://postext.dev/ar/cookbook/piano-score-from-musicxml.md)

## Kurz gesagt

Eine kleine Notenausgabe mit zwei bekannten Klaviermenuetten. Sie zeigt, wie eine aus einem Notensatzprogramm exportierte Datei als Klaviernoten gesetzt wird, die über die Seiten weiterlaufen und spielbar sind.

## Was Sie setzen

Eine Spielausgabe der beiden Menuette aus dem Notenbüchlein für Anna Magdalena Bach von 1725: das G-Dur-Menuett, das jeder Klavierschüler lernt, und sein Gegenstück in g-Moll, heute Christian Petzold zugeschrieben. Die Seite misst 230 × 305 mm, das Format eines Notenalbums. Eine rechte Seite stellt die Stücke in zwei Spalten Libre Caslon unter einem 52-pt-Titel in Bodoni vor. Danach beginnt jedes Menuett auf einer linken Seite und läuft über die Doppelseite, in einem Klaviersystem mit 7 mm hohen Notenzeilen, aus einer MusicXML-Datei in Bravura gestochen: vier Takte je System, Takt 1–16 links und 17–32 rechts, sodass innerhalb eines Stücks nicht umgeblättert wird.

**Dieses Rezept beantwortet:**

- Wie setze ich einen Klaviersatz aus einer MusicXML-Datei über mehrere Seiten?

## Die kurze Antwort

```js
// script.js, Zeilen 36–61
const scoreStyle = {
  staffSize: mm(7), // the rastral of a performing edition: read at arm's length
  musicFont: 'Bravura', // the engraved plate look
  barNumbers: 'system', // a number at the head of each system, as editions print them
  spacing: 1.7, // about four bars to a system, eight systems to a piece
  systemGap: mm(14), // room for the ledger lines above and below the grand staff
  marginTop: pt(LEAD),
  marginBottom: pt(0),
  minSystemsAtBreak: 2, // never one system alone at a foot or the head of a page
  color: col('ink'),
};
// A fence breaks between systems, so a piece runs on over pages; a score resource is kept
// whole. The file is what notation software exports: the Markdown keeps a placeholder
// line in each fence and the pen puts the fetched file there.
const SCORES = {
  'bwv-anh-114': asset('bwv-anh-114.musicxml'),
  'bwv-anh-115': asset('bwv-anh-115.musicxml'),
};
async function withScores(text) {
  const files = await Promise.all(Object.entries(SCORES).map(async ([id, url]) => {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`Score not found (${res.status}): ${url}`);
    return [id, (await res.text()).trim()];
  }));
  return files.reduce((md, [id, xml]) => md.replace(`{{${id}}}`, () => xml), text);
}
```

## Zutaten

**Zeigt**

- [Notensatz](https://postext.dev/de/docs/music.md#noten-im-text): Noten als Text in ```abc-, ```musicxml-, ```mei- oder ```humdrum-Blöcken, von Verovio in Spaltenbreite gestochen und in Systeme geteilt, die über Spalten und Seiten weiterlaufen.
- [Notenstil](https://postext.dev/de/docs/configuration-resources.md#notenstil): scoreStyle und benannte scoreStyles: Systemgröße, Notenschrift, Textschrift, Abstand, Taktzahlen, Systemabstand, Ränder, Farbe und wie viele Systeme bei einem Umbruch zusammenbleiben.

**Verwendet außerdem**

- [Gestaltete Auftakte](https://postext.dev/de/docs/configuration-headings.md#seitenbreite-und-erweiterte-gestaltung)
- [Überschriftenattribute](https://postext.dev/de/docs/document-format.md#attribute-von-überschriften)
- [Überschriftenformate](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Kolumnentitel und Seitenzahlen](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen)
- [Gespiegelte Ränder](https://postext.dev/de/docs/configuration-page-layout.md#gespiegelte-ränder)
- [Kopfzeilen nach Seitenrolle](https://postext.dev/de/docs/configuration-headers-footers.md#textelemente)
- [Spalten in einem Kasten oder im Text](https://postext.dev/de/docs/document-format-directives.md#columns)
- [Absatzformate](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Semantische Farbpalette](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette)
- [PDF-Export](https://postext.dev/de/docs/configuration-pdf-output.md#pdfs-erzeugen)
- [Code-Listings](https://postext.dev/de/docs/configuration-styles.md#code-listings)
- [Im PDF eingebettete Schriften](https://postext.dev/de/docs/configuration-pdf-output.md#warum-ein-font-provider)
- [Kolumnentitel je Abschnitt](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Kapitel ohne Nummer](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)

**Die Konfiguration auf einen Blick**

- [`bodyText`](https://postext.dev/de/docs/configuration-text.md#fließtext), [`colorPalette`](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette), [`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), [`headingStyles`](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile), [`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), [`page`](https://postext.dev/de/docs/configuration-page-layout.md#seite), [`paragraphStyles`](https://postext.dev/de/docs/configuration-styles.md#absatzformate), [`scoreStyle`](https://postext.dev/de/docs/configuration-resources.md#notenstil)

**APIs**

- [`buildDocumentWithFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden), [`decompressWoff2`](https://postext.dev/de/docs/configuration-pdf-output.md#font-provider-im-browser-fontsource--woff2), [`initMusicEngine`](https://postext.dev/de/docs/music.md#noten-im-text), [`renderPageToCanvas`](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern), [`renderToPdf`](https://postext.dev/de/docs/configuration-pdf-output.md#pdfs-erzeugen)

**Schriften**

- Libre Caslon Text (OFL-1.1), Bodoni Moda (OFL-1.1), Archivo Narrow (OFL-1.1)

## Zubereitung

### 1 · Das Stück als MusicXML holen

Die beiden Dateien in `assets/` sind gewöhnliches MusicXML 4.0, das Format, das Dorico, Sibelius, Finale und MuseScore beim Export schreiben (wählen Sie das unkomprimierte `.musicxml`, nicht `.mxl`). Sie enthalten eine Stimme auf zwei Notenzeilen, mit Schlüsseln, Vorzeichen, Balken, der Halsrichtung in den zweistimmigen Takten, Verzierungen und Wiederholungszeichen. Der Stimmenname trägt `print-object="no"`: ein gedrucktes Etikett „Piano“ würde jedes System nach rechts schieben. Der Titel in `<work-title>` wird nicht gedruckt, er dient nur als Beschreibung der Noten; darum gibt das Markdown jedem Stück eine eigene Überschrift.

### 2 · In einen Codeblock setzen, nicht als Ressource

Die [kurze Antwort](https://postext.dev/de/cookbook/piano-score-from-musicxml.md#die-kurze-antwort) lädt jede Datei und schreibt sie in einen ` ```musicxml `-Block, in dem das Markdown eine Platzhalterzeile hält. Ein Block wird in der Breite der Spalte gestochen und in Systeme geteilt, und jedes System wird wie eine Absatzzeile platziert: Das Stück läuft zwischen zwei Systemen auf der nächsten Seite weiter. Eine `score`-Ressource bleibt ganz und könnte keine Seite überqueren. `minSystemsAtBreak: 2` verhindert, dass ein System allein am Fuß einer Seite stehen bleibt oder allein an den Kopf der nächsten wandert.

### 3 · Die Notenzeile für den Spieler bemessen

Die Notenzeile ist 7 mm hoch, so groß wie in einem gedruckten Klavieralbum, das vom Notenpult gelesen wird. `spacing: 1.7` zieht die Noten auseinander, bis ein System auf 186 mm Satzbreite vier Takte enthält; jede Hälfte eines Menuetts braucht damit vier Systeme. `systemGap: 14 mm` lässt Platz für die Hilfslinien zwischen den Händen, und mit diesem Abstand passen vier Systeme unter den Titel der ersten Seite und vier auf die gegenüberliegende: Der Doppelstrich fällt auf den Seitenwechsel der Doppelseite.

### 4 · Jedes Stück auf einer linken Seite eröffnen

```js
// script.js, Zeilen 65–103
const label = (id, content, edge, x, y, extra = {}) => ({
  kind: 'text', id, content, fontFamily: LABEL, fontSize: pt(9), fontWeight: 500,
  color: col('ink'), align: edge.endsWith('right') ? 'right' : 'left',
  placement: { anchor: { to: 'container', edge }, offset: { x: mm(x), y: mm(y) } }, ...extra,
});
const kicker = { fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
  textTransform: 'uppercase', color: col('accent'), align: 'center' };
const centred = (to, y) => ({ anchor: { to, edge: to === 'container' ? 'top' : 'below' },
  offset: { x: mm(0), y: mm(y) }, size: { width: 'fill' } });
const pieceOpener = {
  enabled: true,
  minHeight: mm(34),
  slot: { elements: [
    label('kicker', '{attr.kicker}', 'top', 0, 2,
      { ...kicker, placement: centred('container', 2) }),
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(26),
      fontWeight: 500, color: col('ink'), align: 'center',
      placement: centred('#kicker', 2.5) },
    label('catalogue', '{attr.catalogue}', 'bottom-left', 0, -3),
    label('composer', '{attr.composer}', 'bottom-right', 0, -3, { italic: true }),
  ] },
};
const frontOpener = {
  enabled: true,
  minHeight: mm(78),
  slot: { elements: [
    label('kicker', '{attr.kicker}', 'top', 0, 14,
      { ...kicker, placement: centred('container', 14) }),
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(52),
      fontWeight: 500, color: col('ink'), align: 'center',
      placement: centred('#kicker', 5) },
    { kind: 'text', id: 'composer', content: '{attr.composer}', fontFamily: SERIF,
      fontSize: pt(13), italic: true, color: col('ink'), align: 'center',
      placement: centred('#title', 4) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.75), color: col('accent'),
      placement: { anchor: { to: '#composer', edge: 'below', at: 'center' }, offset: { y: mm(8) },
        size: { width: mm(24) } } },
  ] },
};
```

`breakBefore: { parity: 'even' }` lässt jedes Stück auf einer linken Seite beginnen, und die Titelseite des Herausgebers nutzt einen Überschriftenstil, der auf einer rechten Seite ohne Kolumnentitel öffnet. Werkverzeichnisnummer und Komponist kommen aus den Attributen der Überschrift und stehen in den unteren Ecken des Auftakts, wo gestochene Ausgaben sie drucken.

### 5 · Das Stück im Kolumnentitel der rechten Seite nennen

```js
// script.js, Zeilen 107–120
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: LABEL, fontSize: pt(8), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...extra,
});
const folio = { fontWeight: 700, color: col('ink'), letterSpacing: pt(0) };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('verso-title', '{chapterTitle}', '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: [] };
```

Die erste Seite eines Stücks trägt schon seinen Titel, also bleiben die Kolumnentitel den folgenden Seiten vorbehalten (`pages: 'body'`). Die rechte Seite druckt den Titel des Stücks und die Seitenzahl am Außenrand, in der Auszeichnungsschrift zu 8 pt.

## 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/piano-score-from-musicxml

### script.js

```js
// ═══ Postext Cookbook · Nº 167 · A piano score from MusicXML over several pages ═══════
// https://postext.dev/en/cookbook/piano-score-from-musicxml
// Code: MIT · Text: original (CC BY 4.0) · Music: Mutopia Project (public domain)
// Fonts: Libre Caslon Text, Bodoni Moda, Archivo Narrow (SIL OFL 1.1) · Needs postext ≥ 1.54.0
import {
  buildDocumentWithFonts, renderPageToCanvas, initMusicEngine,
} 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 = 'piano-score-from-musicxml';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a plate-black ink, one deep green accent, every colour linked by id
const palette = {
  ink: '#1b1a18', // the notes and the text: a warm near-black
  accent: '#2f5a46', // titles' kickers, rules: the green of an old music-shop wrapper
  tint: '#eef2ee',
  rule: '#b9c4bc', // hairlines
  muted: '#625f59', // running heads, the colophon
  paper: '#ffffff',
};
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 SERIF = 'Libre Caslon Text';
const DISPLAY = 'Bodoni Moda';
const LABEL = 'Archivo Narrow';
const OUTER = 20; // outer margin in mm: the running heads align to it
const LEAD = 14.5; // body leading in pt

// #region answer: the score style of a performing edition, and a MusicXML file in a fence
const scoreStyle = {
  staffSize: mm(7), // the rastral of a performing edition: read at arm's length
  musicFont: 'Bravura', // the engraved plate look
  barNumbers: 'system', // a number at the head of each system, as editions print them
  spacing: 1.7, // about four bars to a system, eight systems to a piece
  systemGap: mm(14), // room for the ledger lines above and below the grand staff
  marginTop: pt(LEAD),
  marginBottom: pt(0),
  minSystemsAtBreak: 2, // never one system alone at a foot or the head of a page
  color: col('ink'),
};
// A fence breaks between systems, so a piece runs on over pages; a score resource is kept
// whole. The file is what notation software exports: the Markdown keeps a placeholder
// line in each fence and the pen puts the fetched file there.
const SCORES = {
  'bwv-anh-114': asset('bwv-anh-114.musicxml'),
  'bwv-anh-115': asset('bwv-anh-115.musicxml'),
};
async function withScores(text) {
  const files = await Promise.all(Object.entries(SCORES).map(async ([id, url]) => {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`Score not found (${res.status}): ${url}`);
    return [id, (await res.text()).trim()];
  }));
  return files.reduce((md, [id, xml]) => md.replace(`{{${id}}}`, () => xml), text);
}
// #endregion

// #region openers: title centred, the catalogue number left, the composer right
const label = (id, content, edge, x, y, extra = {}) => ({
  kind: 'text', id, content, fontFamily: LABEL, fontSize: pt(9), fontWeight: 500,
  color: col('ink'), align: edge.endsWith('right') ? 'right' : 'left',
  placement: { anchor: { to: 'container', edge }, offset: { x: mm(x), y: mm(y) } }, ...extra,
});
const kicker = { fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
  textTransform: 'uppercase', color: col('accent'), align: 'center' };
const centred = (to, y) => ({ anchor: { to, edge: to === 'container' ? 'top' : 'below' },
  offset: { x: mm(0), y: mm(y) }, size: { width: 'fill' } });
const pieceOpener = {
  enabled: true,
  minHeight: mm(34),
  slot: { elements: [
    label('kicker', '{attr.kicker}', 'top', 0, 2,
      { ...kicker, placement: centred('container', 2) }),
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(26),
      fontWeight: 500, color: col('ink'), align: 'center',
      placement: centred('#kicker', 2.5) },
    label('catalogue', '{attr.catalogue}', 'bottom-left', 0, -3),
    label('composer', '{attr.composer}', 'bottom-right', 0, -3, { italic: true }),
  ] },
};
const frontOpener = {
  enabled: true,
  minHeight: mm(78),
  slot: { elements: [
    label('kicker', '{attr.kicker}', 'top', 0, 14,
      { ...kicker, placement: centred('container', 14) }),
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(52),
      fontWeight: 500, color: col('ink'), align: 'center',
      placement: centred('#kicker', 5) },
    { kind: 'text', id: 'composer', content: '{attr.composer}', fontFamily: SERIF,
      fontSize: pt(13), italic: true, color: col('ink'), align: 'center',
      placement: centred('#title', 4) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.75), color: col('accent'),
      placement: { anchor: { to: '#composer', edge: 'below', at: 'center' }, offset: { y: mm(8) },
        size: { width: mm(24) } } },
  ] },
};
// #endregion

// #region running-heads: the piece on every page after its first, folios outside
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: LABEL, fontSize: pt(8), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...extra,
});
const folio = { fontWeight: 700, color: col('ink'), letterSpacing: pt(0) };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('verso-title', '{chapterTitle}', '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: [] };
// #endregion

const config = {
  locale: t({ en: 'en-us', es: 'es' }),
  colorPalette,
  page: {
    sizePreset: 'custom', width: mm(230), height: mm(305), dpi: 150,
    margins: { top: mm(22), bottom: mm(22), left: mm(24), right: mm(OUTER), mirror: true },
  },
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: SERIF, fontSize: pt(10), 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: DISPLAY, color: col('ink'), fontWeight: 500,
    levels: [
      // Each piece opens on a verso, so it reads across the spread with no page turn.
      { level: 1, fontSize: pt(26), breakBefore: { parity: 'even' }, advancedDesign: pieceOpener },
      { level: 2, fontFamily: LABEL, fontSize: pt(8.5), lineHeight: pt(LEAD), fontWeight: 700,
        letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('accent'),
        marginTop: pt(LEAD), marginBottom: pt(0) },
    ],
  },
  headingStyles: [
    { id: 'front', numbered: false, breakBefore: { parity: 'odd' }, advancedDesign: frontOpener,
      header: { elements: [] }, footer: { elements: [] } },
  ],
  paragraphStyles: [
    { id: 'colophon', fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(10),
      color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
  ],
  scoreStyle,
  header,
  footer,
};

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = await withScores(`---
title: "Two Menuets"
---

# Two Menuets {style="front" kicker="From the Notebook for Anna Magdalena Bach, 1725" composer="Christian Petzold (1677–1733)"}

:::columns{count=2}

## The pieces

In 1725 Johann Sebastian Bach gave his wife Anna Magdalena a bound book of blank music paper. Over the next years she and the rest of the household filled it with keyboard pieces, songs and chorales: two of Bach's own partitas, arias, a few marches and polonaises, and pieces by other composers that the family played at home. The two menuets in this edition stand one after the other in Anna Magdalena's hand, the first in G major, the second in G minor.

For two centuries both were taken for Bach's own work. The Bach-Werke-Verzeichnis lists them among the doubtful works, as Anh. 114 and Anh. 115, and the first became one of the best-known pieces of piano teaching under his name. In the 1970s both were found in a harpsichord suite by Christian Petzold, an organist in Dresden, and they are now given to him.

Each menuet has 32 bars in two halves of 16, and each half is repeated. The first stays close to G major and visits D major in its second half; the second answers it in G minor and turns to B flat major at its double bar. Paired dances of the period were often played as a set: Menuet I, Menuet II, then Menuet I again without its repeats.

## This edition

The notes follow the edition of the Bach-Gesellschaft, as entered by Allen Garvin for the Mutopia Project, which placed it in the public domain. They were converted to MusicXML 4.0 and proofread bar by bar against that source: pitches, rhythms, accidentals, the two voices of the closing bars and the beaming. The ornaments are the source's, and so are the grace note in bar 8 and the reminder natural in bar 24 of Menuet I.

Neither the source nor this edition gives fingering, dynamics or slurs. The margins leave room to add them in pencil.

## Performance notes

A menuet of this kind is felt in one beat to the bar rather than three, at around crotchet = 120 to 132. Crotchets sound well slightly detached and quavers joined, which is how harpsichordists phrase the piece; on a piano, use the pedal sparingly, at the cadences if at all.

The mordent, the sign with a stroke through it, is the main note, the note below and the main note again, quick and on the beat. The short trill without a stroke, in bar 30 of Menuet I and in bars 8, 9, 15 and 31 of Menuet II, starts on the note above, on the beat.

Each menuet is set across a pair of facing pages, so no page is turned inside a piece. The one turn comes between the two menuets.

:::paragraphs{style="colophon"}
Set in Libre Caslon Text, Bodoni Moda and Archivo Narrow (SIL OFL). Music in Bravura (SIL OFL), engraved by Verovio. Notes after the Mutopia Project (public domain).
:::

:::

# Menuet in G major {kicker="Menuet I" catalogue="BWV Anh. 114" composer="Christian Petzold"}

\`\`\`musicxml {alt="Menuet in G major, BWV Anh. 114, by Christian Petzold, for keyboard"}
{{bwv-anh-114}}
\`\`\`

# Menuet in G minor {kicker="Menuet II" catalogue="BWV Anh. 115" composer="Christian Petzold"}

\`\`\`musicxml {alt="Menuet in G minor, BWV Anh. 115, by Christian Petzold, for keyboard"}
{{bwv-anh-115}}
\`\`\`
`); // content.<lang>.md, then the two files

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Libre Caslon Text': ['400', '400i', '700'],
  'Bodoni Moda': ['500'],
  'Archivo Narrow': ['400', '500', '500i', '600', '700'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await initMusicEngine(); // the engraver (Verovio) loads only for books with music
const doc = await buildDocumentWithFonts({ markdown }, config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Two Menuets', es: 'Dos minuetos' }) });
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 ───────────────────────────────────────────────────────────────────────
```

## Varianten

### Eine Studienausgabe mit kleinerer Notenzeile setzen

Eine 6-mm-Notenzeile mit dem eigenen Abstand des Notenstechers fasst sieben oder acht Takte je System, sodass jedes Menuett auf seine erste Seite passt.

```diff
-  staffSize: mm(7),
+  staffSize: mm(6),
-  spacing: 1.7,
+  spacing: 1,
```

### Kurze Beispiele nummerieren

Besteht die Musik aus kurzen Beispielen, die der Text zitiert, passt eine nummerierte `score`-Ressource besser: [vierstimmige Choräle als nummerierte Notenbeispiele](https://postext.dev/de/cookbook/conservatory-method-chorales.md).

## Häufige Fehler

- Systemumbrüche, die in der Datei stehen (`<print new-system="yes">`), bleiben nicht erhalten: Die Systeme füllen die Spaltenbreite, also stellen Sie `spacing` und `staffSize` ein, bis sie die gewünschten Takte enthalten.
- Der Notenstecher verteilt die Takte nicht gleichmäßig auf die Systeme, und das letzte System wird in seiner natürlichen Breite gesetzt: Bei 6 mm und `spacing: 1` steht Takt 32 allein in einem System. Ändern Sie `spacing`, bis das letzte System mehr als einen Takt enthält.
- Eine komprimierte `.mxl`-Datei ist ein Zip-Archiv: Entpacken Sie sie oder exportieren Sie das unkomprimierte Format, bevor Sie sie laden.

## Nachweise

- Rezept: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Text: The introduction and performance notes, written for this edition: Ignacio Ferro, CC-BY-4.0
- Bilder: Menuet in G major, BWV Anh. 114: the notes of the Bach-Gesellschaft edition as entered for the Mutopia Project (Mutopia-2017/01/19-75), converted to MusicXML 4.0 and proofread bar by bar for this edition: Christian Petzold; Allen Garvin (Mutopia Project) ([Quelle](https://www.mutopiaproject.org/cgibin/piece-info.cgi?id=75)), gemeinfrei
- Bilder: Menuet in G minor, BWV Anh. 115: the notes of the Bach-Gesellschaft edition as entered for the Mutopia Project (Mutopia-2015/08/21-76), converted to MusicXML 4.0 and proofread bar by bar for this edition: Christian Petzold; Allen Garvin (Mutopia Project) ([Quelle](https://www.mutopiaproject.org/cgibin/piece-info.cgi?id=76)), gemeinfrei
- Schrift: Libre Caslon Text (OFL-1.1), Bodoni Moda (OFL-1.1), Archivo Narrow (OFL-1.1)
- Code: MIT · Beispielinhalt: CC-BY-4.0

## Verwandte Rezepte

- [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º 164 · Ein Liederbuch mit Melodie, Text und Akkorden](https://postext.dev/de/cookbook/songbook-melody-words-chords.md): Jedes Lied ist ein ABC-Block: die Melodie, Akkordsymbole über den Noten und die erste Strophe darunter, in Spaltenbreite gestochen nach einem Notenstil. · Stufe 2 (Mittel) · Lyrik, Einzelblätter und Akzidenzen
- [Nº 165 · MEI-Notenbeispiele mit kritischem Apparat in den Fußnoten](https://postext.dev/de/cookbook/musicology-mei-apparatus.md): MEI-5-Blöcke im Text, gestochen auf einem 5-mm-System, Lesarten mit app, lem und rdg kodiert und der Apparat nach Takt und Note in den Fußnoten. · Stufe 2 (Mittel) · Fachartikel und Wissenschaft
