In short
A chapter of a music theory textbook about how phrases end. The music examples are written as short lines of text in the chapter, and Postext draws them as staves and notes; the longest one continues on the next page like a paragraph.
What you'll build
Chapter 5 of a keyboard harmony course, 180 × 250 mm in Libre Caslon, on the five cadences: perfect and imperfect authentic, half, plagal and deceptive. Each cadence gets two short paragraphs and an example in three voices with its Roman numerals under the staff, written in ABC right in the Markdown. The chapter ends with Greensleeves in A minor, a numeral over every bar, and that melody is too long for the room left on page 3: it sets three systems there and the last two at the head of page 4, broken between systems as a paragraph breaks between lines. Staff size, music font and spacing are set once in the configuration, so the five examples and the melody share one look.
This recipe answers
- How do I put music examples in the text of a textbook, written as notation?
The short answer
// Each ```abc fence in the chapter is engraved at the width of the text column and cut
// into systems. The style is set once here; the Markdown only holds the notation.
const scoreStyle = {
staffSize: mm(5.5), // a textbook's rastral: examples read at arm's length on a desk
musicFont: 'Bravura', // the traditional engraved look, heavy enough beside Caslon
textFont: 'Libre Caslon Text', // the Roman numerals of `w:` lines, in the text face
spacing: 1.6, // air between the chords: an example reads as a progression, not a cluster
barNumbers: 'system',
systemGap: mm(4),
marginTop: pt(LEAD / 2),
marginBottom: pt(LEAD / 2),
color: col('ink'),
// A long example runs on into the next page between two systems, never leaving one
// system alone at the foot of a page or at the head of the next.
minSystemsAtBreak: 2,
};
One score style for every example in the text
Ingredients
- Features
- Music notationScore styleDesigned openersHeading attributesRunning heads and foliosMirrored marginsHeads by page roleCallout boxesParagraph stylesSemantic colour paletteWidows, orphans and runtsPDF export
- Type
- Libre Caslon Text, Libre Caslon Display, Libre Franklin (SIL OFL 1.1)
- Assets
- None: every picture is drawn in code
Method
#1 · Write the example where the text needs it
A fence tagged abc holds the notation, and the text goes on after it. L:1/2 makes each bare chord a half note, [Bdg] stacks three notes in one voice, and a w: line puts one word under each chord, here the Roman numerals. The alt attribute is what a screen reader and the tagged PDF say instead of the picture.
```abc {alt="Perfect authentic cadence in G major: I, IV, V, I"}
X:1
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [Adf] [Bdg] |]
w: I IV V I
```#2 · Give every score the book's look
The short answer is the whole design of the music: a 5.5 mm staff, as harmony books use for examples read at the keyboard, Bravura beside the Caslon text, and the numerals in the text face instead of the engraver's Times. spacing: 1.6 spreads four half-note chords over a third of the measure, so a progression reads chord by chord. The same setting decides how many bars of Greensleeves fit on a system: three, in a 125 mm measure.
#3 · Let a long example run on
minSystemsAtBreak: 2 is what keeps Greensleeves readable across the turn. The melody comes out as five systems; three fit at the foot of page 3, so two go to page 4, and neither page is left with a single system. A score with fewer than four systems moves whole to the next page, as the short cadence examples would.
#4 · Start the engraver before the build
// The engraver (Verovio) loads only for books with music: start it before the build,
// or every score is laid out as a grey placeholder box.
await initMusicEngine();
const doc = await buildDocumentWithFonts({ markdown }, config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Harmony textbook', es: 'Manual de armonía' }) });
// The PDF draws every system as vector paths, the numerals in the embedded text face.
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);
Verovio is about 3.7 MB of WebAssembly, so the engine loads it only when you ask. Without initMusicEngine() the layout still runs, and every score is a grey box of the right size. renderToPdf needs nothing more: the PDF draws each system as vector paths and sets the numerals in the embedded Caslon.
#5 · Open the chapter with a sunk title
const opener = {
enabled: true,
minHeight: mm(48),
slot: { elements: [
{ kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontSize: pt(8.5),
fontWeight: 700, letterSpacing: pt(1.7), textTransform: 'uppercase', color: col('accent'),
align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { x: mm(0), y: mm(12) } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Libre Caslon Display',
fontSize: pt(34), lineHeight: 1.05, color: col('ink'), align: 'left', overflow: 'wrap',
placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
size: { width: mm(110), height: 'auto' } } },
{ kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5),
color: col('accent'),
placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
size: { width: mm(16) } } },
] },
};
The opener takes its kicker from the heading's kicker attribute and reserves 48 mm, enough for the title and its rule with the first example still on page 1.
The whole recipe
// ═══ Postext Cookbook · Nº 163 · Harmony textbook with ABC examples ═════════════
// https://postext.dev/en/cookbook/harmony-textbook-abc-examples
// Code: MIT · Text: original (CC BY 4.0) · Music: Greensleeves (traditional, PD)
// Fonts: Libre Caslon Text & Display, Libre Franklin (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 = 'harmony-textbook-abc-examples';
// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a conservatory green for the accents, warm ink for text and notes
const palette = {
ink: '#1f1c19', // text and notation: a warm near-black
accent: '#2f5d4f', // kickers, rules, Roman numerals in the table
tint: '#edf2ef', // the box of key terms
rule: '#c3cfc9', // hairlines
muted: '#5c6863', // running heads, 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' } })),
// The engine's defaults link to main-color: point it at the accent, never the default blue.
{ id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
// #endregion
const TRIM = { width: 180, height: 250 };
const MARGIN = { top: 22, bottom: 24, inner: 22, outer: 33 }; // a 125 mm measure
const LEAD = 14.5; // body leading in pt: the baseline grid
const LABEL = 'Libre Franklin';
// #region answer: one score style for every example in the text
// Each ```abc fence in the chapter is engraved at the width of the text column and cut
// into systems. The style is set once here; the Markdown only holds the notation.
const scoreStyle = {
staffSize: mm(5.5), // a textbook's rastral: examples read at arm's length on a desk
musicFont: 'Bravura', // the traditional engraved look, heavy enough beside Caslon
textFont: 'Libre Caslon Text', // the Roman numerals of `w:` lines, in the text face
spacing: 1.6, // air between the chords: an example reads as a progression, not a cluster
barNumbers: 'system',
systemGap: mm(4),
marginTop: pt(LEAD / 2),
marginBottom: pt(LEAD / 2),
color: col('ink'),
// A long example runs on into the next page between two systems, never leaving one
The composed script.js runs as it is: paste it into any page’s module script, or open the recipe on CodePen. Recipe folder on GitHub ↗ (opens in a new tab)
Variations
#Use Leland for a lighter page
Leland, MuseScore's font, has thinner stems and smaller noteheads than Bravura; it suits a book set in a light text face.
- musicFont: 'Bravura',
+ musicFont: 'Leland',#Write a second voice in another format
The ABC reader takes one voice. For a soprano and bass on separate staves, write the example in Humdrum or MusicXML; the conservatory method sets four-part chorales that way.
Pitfalls
- Write a chord's length inside the brackets,
[B2d2g2], or setL:to the chord's value: a length after the bracket,[Bdg]2, is read as the default length. - Verovio's ABC reader gives each syllable of a
w:line to the next note and ignores*and_, so aw:line cannot skip notes. To mark a few bars, write the marks as chord symbols over the staff, as the Greensleeves example does. - A text face rarely has the ♯ and ♭ signs: write "F-sharp" in the prose, or the browser sets the sign from a font of the system.
Credits
- Recipe
- Ignacio Ferro
- Text
- The chapter on cadences and its examples, written for this recipe · Ignacio Ferro · CC BY 4.0
- Greensleeves, English tune of the sixteenth century, in its common modern form · Traditional · public domain
- Fonts
- Libre Caslon Text (SIL OFL 1.1) · Libre Caslon Display (SIL OFL 1.1) · Libre Franklin (SIL OFL 1.1)
Edit this write-up ↗ (opens in a new tab)Recipe folder on GitHub ↗ (opens in a new tab)


