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
// 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
- Features
- Music notationScore styleVerse line by lineDesigned openersHeading attributesNumbered headingsHeading stylesUnnumbered chaptersCovers, title pages and colophonsDocument metadataTable of contentsRunning heads and foliosHeads by page roleParagraph stylesMirrored marginsSemantic colour palettePaper colourText, rules and boxes in page designsPDF export
- Also uses
- Columns in a box or in the textCode listingsFonts embedded in the PDFRunning heads per section
- 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
// # 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
// :::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
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
// ═══ 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 aw: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
- Recipe
- Ignacio Ferro
- Text
- Amazing Grace: words (1779, five of the six stanzas, in the Olney Hymns spelling) · John Newton · public domain
- Amazing Grace: the tune New Britain (Southern Harmony, 1835), melody as in E. O. Excell's setting of 1900, chord symbols added · Traditional American; William Walker; Edwin O. Excell · public domain
- Oh My Darling, Clementine: words and tune (Oliver Ditson, Boston, 1884) · Percy Montrose · public domain
- Auld Lang Syne: words (1788) and the Scots tune of George Thomson's Select Songs of Scotland (1799) · Robert Burns; traditional · public domain
- Cumpleaños feliz: the tune of Good Morning to All (Song Stories for the Kindergarten, Chicago, 1893); Spanish words traditional · Mildred J. Hill; traditional · public domain
- Martinillo: the French tune of Frère Jacques (18th century) with the traditional Spanish words · Traditional · public domain
- La cucaracha: tune, chorus and verse as in the Spanish Wikipedia transcription; two traditional Mexican coplas · Traditional · public domain
- The preface, the notes on the songs and the chord symbols · Ignacio Ferro · CC BY 4.0
- Fonts
- Spectral (SIL OFL 1.1) · Spectral SC (SIL OFL 1.1) · Fraunces (SIL OFL 1.1)
Edit this write-up ↗ (opens in a new tab)Recipe folder on GitHub ↗ (opens in a new tab)


