Skip to main content
Recipe number 164

Cookbook · Chapter 9 · Complete publications

A songbook with melody, words and chords

Each song is an ABC fence: the melody, chord names over the staff and the first verse under the notes, engraved at the column's width by one score style.

On this page
Output
Canvas · PDF
Postext
Tested with Postext 1.54.0
Needs ≥ 1.54.0 · postext-pdf ≥ 1.54.0
Licence
Updated 22 Oct 2026
Code MIT · Text CC BY 4.0

pp. 2–3 of 6

  • Trim 148 × 210 mm
  • 1 column
  • Spectral 10.3/14
  • Fraunces
  • Spectral SC
  • 6 pages
  • Level
  • Postext 1.54.0
  • Laid out in 300 ms
  • 164 lines of code

In short

A small songbook for singing round a table. Each song is written as plain text in a music notation called ABC, and Postext draws the staff, the notes, the guitar chords and the words under the notes. The other verses follow in two columns.

What you'll build

Songs for the Long Table, a pocket songbook on an A5 trim: a madder title page, a note to the singers with the contents, three songs and the notes on where each one comes from. Every song opens under a large madder number, a Fraunces title and a small-capitals credit line. Below it the melody runs on a 5.5 mm staff in Leland, with the guitar chords over the bars and the first verse under the notes in Spectral, the book's text face. The remaining verses stand in two columns, so a singer reads straight down from the last bar. Amazing Grace, whose long lines would turn over in a column, sets its verses one under another, centred. The Spanish edition is a birthday songbook with three shorter songs, two to a page.

This recipe answers

  • How do I set a songbook with the melody, the words under the notes and chord symbols?

The short answer

script.js · lines 33–50in full code
// A song is an ```abc fence in the Markdown: the melody, its chords written "G" before the note
// they fall on, and the first verse on a w: line under it. The look is set here, once.
const scoreStyle = {
  staffSize: mm(5.5), // a songbook rastral; the words under it come out at about 8 pt
  musicFont: 'Leland', // lighter noteheads than Bravura, close to Spectral's colour
  textFont: TEXT, // words and chord names in the text face, not the engraver's Times
  spacing: 1, // the engraver's own: five or six bars of 3/4 to a system on this measure
  barNumbers: 'none', // a table of singers counts verses, not bars
  systemGap: mm(4.5), // room for the chord names between one system's words and the next
  marginTop: pt(4),
  marginBottom: pt(LEAD),
  color: col('ink'),
  minSystemsAtBreak: 2, // never a lone system at a page foot or head
};
// Two habits of Verovio's ABC reader shape the w: lines. Every sung note takes the next
// syllable in order: the `_` and `*` holds are read as nothing, so the second note of a
// slurred pair (B/G/ under "-ing") gets a no-break space, which prints nothing. A tied note
// takes none. `~` joins two syllables on one note: "de~a-trás".

The score style: a 5 mm staff in Leland, the words in the book's face

Ingredients

Type
Spectral, Spectral SC, Fraunces (SIL OFL 1.1)
Assets
None: every picture is drawn in code

Method

#1 · Write the song, style the score once

The score style is the short answer above. In the Markdown each song is an ```abc fence: the header lines give the metre, the note length and the key; a chord name in double quotes ("D7"A) prints over the note it stands before; a w: line under the music puts one syllable under each note, split with hyphens. A tune with a verse and a chorus is two music lines, each with its own w: line. Staff size, music font, the face of the words, the bar numbers and the gap between systems come from scoreStyle, so the three songs match and the fences hold nothing but music.

#2 · Open each song under its number and credit

script.js · lines 66–81in full code
// # Amazing Grace {credit="Words John Newton, 1779 · Tune New Britain, American, 1835"}
// The songs run on, three lines apart: a short song shares its page with the next one.
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const text = (id, content, font, size, color, placement, extra = {}) => ({ kind: 'text', id,
  content, fontFamily: font, fontSize: pt(size), color: col(color), align: 'left', placement,
  ...extra });
const songHead = { enabled: true, minHeight: mm(21), slot: { elements: [
  text('number', '{number}', DISPLAY, 26, 'madder', at('container', 'top-left', 0, 1),
    { fontWeight: 600, lineHeight: 1 }),
  text('title', '{titleText}', DISPLAY, 21, 'ink', { ...at('container', 'top-left', 12, 2),
    size: { width: mm(100) } }, { fontWeight: 600, lineHeight: 1.05 }),
  text('credit', '{attr.credit}', LABEL, 8.4, 'madder', { ...at('#title', 'below', 0, 2.2),
    size: { width: mm(100) } }, { fontWeight: 600, letterSpacing: pt(0.6) }),
] } };
const songs = { level: 1, numberingTemplate: '{1}', advancedDesign: songHead,
  marginTop: pt(LEAD * 3), marginBottom: pt(0), breakBefore: { enabled: false } };

The song is a first-level heading with a credit attribute, and its design sets the number, the title and the credit line from it. minHeight gives every song the same 21 mm head, so the staves start at the same depth on each page. The songs run on with three lines of space above each title, and a score is never split before its second system (minSystemsAtBreak: 2): the English songs fill a page each, and two of the short Spanish songs share one.

#3 · Set the other verses in two columns

script.js · lines 54–62in full code
// :::columns{count=2} holds one :::verse block per stanza; each line of the stanza is a
// line of Markdown, the turned lines indented two spaces (one em). A hymn in long lines
// (Amazing Grace) drops the columns: its stanzas stand one under another, centred.
const verse = { id: 'verse', fontSize: pt(9.4), lineHeight: pt(13), hyphenation: false,
  marginBottom: pt(13 / 2) };
const chorus = { id: 'chorus', fontSize: pt(9.4), lineHeight: pt(13), italic: true,
  hyphenation: false, marginBottom: pt(LEAD / 2) };
const colophon = { id: 'colophon', fontFamily: LABEL, fontSize: pt(7.8), lineHeight: pt(11),
  color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD * 2) };

Each stanza is a :::verse block, so its lines break where the poet broke them and the second and fourth lines indent by one em. A :::columns{count=2 gap=4mm} group sets them side by side at 9.4 pt. Count the characters first: Burns's lines fit a 55 mm column, Newton's long metre does not, so Amazing Grace keeps a single centred :::verse block and lets the blank lines part its stanzas.

#4 · Keep the furniture quiet

script.js · lines 119–131in full code
const head = (id, content, parity, edge, x, style = {}) => ({
  kind: 'text', id, content, parity, pages: 'all', // a song's first page carries them too
  fontFamily: LABEL, fontSize: pt(8.4), fontWeight: 600, letterSpacing: pt(0.9),
  color: col('muted'), ...style,
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12.5) } },
});
const folio = { fontFamily: TEXT, fontWeight: 600, letterSpacing: pt(0), color: col('madder') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('verso-title', '{title}', 'even', 'top-left', OUTER + 8),
  head('recto-song', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 8)),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };

The running heads are on every page the songs reach, the book title on the verso and the song on the recto, with the folio in the madder of the song numbers. The title page and the note to the singers clear them with an empty header in their heading styles.

The whole recipe

Sandbox
// ═══ Postext Cookbook · Nº 164 · Songbook: melody, words under the notes, chords ═══════
// https://postext.dev/en/cookbook/songbook-melody-words-chords
// Code: MIT · Words and tunes: public domain (see the notes in each edition) · Prose: CC BY 4.0
// Fonts: Spectral, Spectral SC, Fraunces (SIL OFL 1.1); Leland (music) · 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 = 'songbook-melody-words-chords';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { // every colour in the config links to one of these
  ink: '#211e1b', // text and notes: a warm near-black
  madder: '#8a3324', // the one accent: song numbers, the title page, credit lines
  tint: '#f1e7d8', // the subtitle on the madder ground (8.2:1)
  rule: '#cbbca6', // hairlines
  muted: '#675e55', // running heads, notes and the colophon (5.9:1 on the paper)
  paper: '#fbf8f1', // a cream stock
};
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, so nothing prints blue.
  { id: 'main-color', name: 'madder (defaults)', value: { hex: palette.madder, model: 'hex' } },
];
const [TEXT, LABEL, DISPLAY] = ['Spectral', 'Spectral SC', 'Fraunces'];
const LEAD = 14; // pt: the body's leading
const OUTER = 15; // mm: the outer margin, where the running heads and folios align

// #region answer: the score style: a 5 mm staff in Leland, the words in the book's face
// A song is an ```abc fence in the Markdown: the melody, its chords written "G" before the note
// they fall on, and the first verse on a w: line under it. The look is set here, once.
const scoreStyle = {
  staffSize: mm(5.5), // a songbook rastral; the words under it come out at about 8 pt
  musicFont: 'Leland', // lighter noteheads than Bravura, close to Spectral's colour
  textFont: TEXT, // words and chord names in the text face, not the engraver's Times
  spacing: 1, // the engraver's own: five or six bars of 3/4 to a system on this measure
  barNumbers: 'none', // a table of singers counts verses, not bars
  systemGap: mm(4.5), // room for the chord names between one system's words and the next
  marginTop: pt(4),
  marginBottom: pt(LEAD),
  color: col('ink'),
  minSystemsAtBreak: 2, // never a lone system at a page foot or head
};
// Two habits of Verovio's ABC reader shape the w: lines. Every sung note takes the next
// syllable in order: the `_` and `*` holds are read as nothing, so the second note of a

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

#Engrave in Bravura

Bravura has heavier noteheads and the look of an engraved plate; it suits a hymnal set in a darker text face.

-  musicFont: 'Leland', // lighter noteheads than Bravura, close to Spectral's colour
+  musicFont: 'Bravura',

#Number the bars

A choir rehearses by bar numbers; print one at the start of every system.

-  barNumbers: 'none', // a table of singers counts verses, not bars
+  barNumbers: 'system',

Pitfalls

Pitfall

A heading style inherits its level's page break

A headingStyles entry takes every field it leaves out from its heading level, breakBefore included. A contents page or a colophon styled on an H1 after a :::pagebreak inherits parity 'odd' and lands behind a blank page. Give such a style breakBefore: { enabled: false }. Heading styles →

  • Verovio's ABC reader ignores the _ and * hold marks of a w: line: every note takes the next syllable. Under the second note of a slur, write a no-break space (U+00A0) as a syllable of its own; it prints nothing. Tied notes take no syllable.
  • ~ joins two syllables under one note (de~a-trás), but the engraver prints them with a plain space, not a tie.
  • Write chord names without ♭ or ♯ when you can: most text faces have no such signs and the page falls back to a system font. Choose keys whose chords are naturals (G, C, D7, Em), or spell B-flat in the prose.

Credits

Text
Fonts
Spectral (SIL OFL 1.1) · Spectral SC (SIL OFL 1.1) · Fraunces (SIL OFL 1.1)
SandboxPDF