In short
A small printed guide to five paintings. Wherever the text mentions a web page, a small number leads to the foot of the page, where a square code opens that page on a phone.
What you'll build
A pocket guide to the Dutch rooms of the Metropolitan Museum of Art: five paintings, an hour on foot, one painting to a page. The cover is Vermeer's young woman at the window, cut off at the top edge. Each stop opens on its gallery number, title and painter, then the picture with its wall label beside it, then three short paragraphs. The text links to the museum's catalogue as any web page would. On paper each link becomes a small number, and at the foot of the page the note gives the short address and a QR code that a visitor holds a phone over, standing in front of the painting.
This recipe answers
- How do I print the web addresses of a book's links, as numbered notes or QR codes a reader can scan?
The short answer
const links = {
print: 'notes', // a note marker after the link text, the address in a note at the foot
show: 'both', // the short address for reading, the code for the phone
urlDisplay: 'short', // metmuseum.org/art/collection/search/436616: no https://, no www.
repeat: 'first', // an address noted once is cited again by its number, never noted twice
qr: { size: mm(16), side: 'start', gap: em(1.2), color: col('ink') },
};
// Link notes are footnotes: they take the note style and the numbering below. 'document'
// numbers them through the walk, so the five stops print notes 3 to 7, not five notes 1.
const footnotes = { placement: 'column', numbering: 'document', fontSize: pt(8),
lineHeight: pt(10.5), color: col('muted'), spaceAbove: pt(LEAD), spaceBetween: pt(5),
separator: { enabled: true, width: 0.18, lineWidth: pt(0.5), color: col('rule') } };
Every web link of the guide prints as a numbered note with a code to scan
Ingredients
- Features
- Link addresses in printFootnotesDesigned openersHeading attributesPictures in page designsGuide words in running headsRunning heads and foliosHeads by page roleFigures exactly hereSide captionsCustom resource typesSource and credit linesSemantic colour palettePDF export
- Also uses
- Full-width chapter bandParagraph stylesFonts embedded in the PDFFigures and tables as resources
- Type
- Newsreader, Instrument Serif, Archivo (SIL OFL 1.1)
- Assets
hals-1200.jpgrembrandt-1200.jpgruisdael-1400.jpgsteen-1200.jpgvermeer-1600.jpgvermeer-cover-1800.jpg- Johannes Vermeer, Young Woman with a Water Pitcher, about 1662 (the cover, cropped) (The Metropolitan Museum of Art, CC0 1.0)
- Frans Hals, Young Man and Woman in an Inn, 1623 (stop 1) (The Metropolitan Museum of Art, CC0 1.0)
- Jan Steen, Merry Company on a Terrace, about 1670 (stop 2) (The Metropolitan Museum of Art, CC0 1.0)
- Rembrandt, Aristotle with a Bust of Homer, 1653 (stop 3) (The Metropolitan Museum of Art, CC0 1.0)
- Johannes Vermeer, Young Woman with a Water Pitcher, about 1662 (stop 4) (The Metropolitan Museum of Art, CC0 1.0)
- Jacob van Ruisdael, Wheat Fields, about 1670 (stop 5) (The Metropolitan Museum of Art, CC0 1.0)
Method
#1 · Write links, print notes
The code is the short answer above. The Markdown keeps ordinary links, [The catalogue entry](https://www.metmuseum.org/…/436616), and links.print: 'notes' turns each one into a numbered note with the address and the code (links in print). The notes are footnotes: numbering: 'document' counts them through the walk, so the reader meets 1 to 7, and repeat: 'first' lets the last stop cite the Hals by its note 3 instead of printing the same code twice. In the PDF the code is drawn as vectors and is itself a link.

#2 · One stop to a page
const stop = { enabled: true, minHeight: pt(4 * LEAD), slot: { elements: [
{ kind: 'text', id: 'stop', content: '{attr.stop}', ...caps(7.5), color: col('delft'),
align: 'left', placement: at('container', 'top-left', 0, 0) },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(22),
lineHeight: 1.05, color: col('ink'), align: 'left', placement: at('#stop', 'below', 0, 1.6) },
{ kind: 'text', id: 'artist', content: '{attr.artist}', fontFamily: TEXT, italic: true,
fontSize: pt(10.5), color: col('muted'), align: 'left',
placement: at('#title', 'below', 0, 0.8) },
] } };
Each stop is a second-level heading whose breakBefore opens a page, either side, so a visitor turns one leaf per painting. The gallery number and the painter are heading attributes; the design sets them above and below the title, and minHeight keeps the picture at the same height on every page.
#3 · The wall label beside the picture
// Paintings are not numbered: their own type, with no caption prefix and no number.
const paintingType = { id: 'painting', name: t({ en: 'Painting', es: 'Cuadro' }),
shortLabel: t({ en: 'painting', es: 'cuadro' }), captionPrefix: '', numberingTemplate: '',
resetOn: 'never', counterFormat: 'decimal' };
// Set where the ::resource line stands, under the stop's heading; caption and note beside it.
const HERE = { position: 'here', captionSide: 'inside' };
const cm = (n) => (LANG === 'es' ? String(n).replace('.', ',') : String(n)); // decimal comma
// The wall label: medium, size in cm (no-break spaces) and the museum's credit line.
const painting = (id, w, h, [cmH, cmW], credit, alt, width = 0.5) => ({
id, typeId: 'painting', kind: 'bitmap', createdAt: 0, updatedAt: 0, placement: { ...HERE, width },
bitmap: { fileId: `${id}-${Math.max(w, h)}.jpg`, format: 'jpeg', width: w, height: h },
caption: `${OIL}, ${cm(cmH)}\u00a0×\u00a0${cm(cmW)}\u00a0cm. ${credit}`, altText: alt,
note: 'The Metropolitan Museum of Art, Open Access (CC0)' });
const OIL = t({ en: 'Oil on canvas', es: 'Óleo sobre lienzo' });
const ALTMAN = t({ en: 'Bequest of Benjamin Altman, 1913', es: 'Legado de Benjamin Altman, 1913' });
const resources = [
painting('hals', 908, 1200, [105.4, 79.4], `${ALTMAN} (14.40.602)`, t({
en: 'A laughing young man raises a glass; a young woman leans on his shoulder.',
es: 'Un joven ríe y alza una copa; una joven se apoya en su hombro.' }), 0.4), // portrait
painting('steen', 1115, 1200, [141, 131.4], 'Fletcher Fund, 1958 (58.89)', t({
en: 'A crowded party under a vine, with a lute player, a child and a dog.',
es: 'Una fiesta concurrida bajo una parra, con un laudista, un niño y un perro.' })),
painting('rembrandt', 1139, 1200, [143.5, 136.5], t({
en: 'Purchase, special contributions and funds given or bequeathed by friends of the Museum',
es: 'Compra con aportaciones y fondos donados o legados por amigos del Museo' })
+ ', 1961 (61.198)', t({ en: 'A bearded man in a black hat rests his hand on a marble bust.',
es: 'Un hombre barbado con sombrero negro apoya la mano en un busto de mármol.' })),
painting('vermeer', 1422, 1600, [45.7, 40.6], t({ en: 'Marquand Collection, gift of',
es: 'Colección Marquand, donación de' }) + ' Henry G. Marquand, 1889 (89.15.21)', t({
en: 'A young woman at an open window holds a gilt pitcher over a basin.',
es: 'Una joven junto a una ventana abierta sujeta una jarra dorada sobre una jofaina.' })),
painting('ruisdael', 1400, 1084, [100, 130.2], `${ALTMAN} (14.40.623)`, t({
en: 'A sandy road between wheat fields under a tall bank of cloud.',
es: 'Un camino de arena entre campos de trigo bajo un alto banco de nubes.' }), 0.6),
// The cover: the Vermeer cut to the box's 4 : 3 (160 × 120 mm), since the opener fits a
// picture inside its box. Drawn by the cover design, never cited.
{ ...painting('vermeer-cover', 1800, 1350, [], '', t({
en: 'Detail of Vermeer’s young woman at the window, in blue and white.',
es: 'Detalle de la joven de Vermeer junto a la ventana, en azul y blanco.' })),
caption: '', note: '' },
];
captionSide: 'inside' sets the caption and its credit note beside the picture, as a museum hangs its label, and width gives each picture its share of the measure: less for the tall Hals, more for the wide Ruisdael, so the five pages keep a similar height of text. The label is built from the medium, the size in centimetres with no-break spaces and the museum's credit line, with a decimal comma in the Spanish edition.
#4 · The stop in the running head
const head = (id, content, parity, edge, x, extra = {}) => ({
kind: 'text', id, content, parity, pages: 'body', ...caps(7, 600), color: col('muted'),
placement: at('page', edge, x, 13), ...extra,
});
const folio = { ...caps(8, 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-stop', '{firstMark.h2}', 'odd', 'top-right', -(OUTER + 9)),
head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };
{firstMark.h2} prints the title of the stop that starts on the page, so the recto names the painting and the verso the guide. No running head reaches the cover, a page whose role is the opener.
The whole recipe
// ═══ Postext Cookbook · Nº 175 · A museum walk whose links print as QR notes ═════
// https://postext.dev/en/cookbook/museum-guide-qr-link-notes
// Code: MIT · Text: original (CC BY 4.0) · Paintings: The Metropolitan Museum of Art (CC0)
// Fonts: Newsreader, Instrument Serif, Archivo (SIL OFL 1.1) · Needs postext ≥ 1.65.0
// A pocket gallery guide: five paintings, one to a page. The Markdown writes the museum's
// catalogue pages as ordinary links; the print edition turns each one into a numbered note at
// the foot of the page, with the short address and a code a phone can scan.
import {
buildDocumentWithFonts, renderPageToCanvas, registerResourceImage,
} 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 = 'museum-guide-qr-link-notes';
// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: Delft blue on warm paper, every colour linked by id
const palette = {
ink: '#1f1d1a', // text: a warm near-black
delft: '#2b4a7a', // the one accent: kickers, stop numbers, rules (8.6:1 on paper)
rule: '#c6c9cf', // hairlines
muted: '#5f6168', // running heads, notes, credits (6.1:1 on paper)
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, so nothing prints blue.
{ id: 'main-color', name: 'delft (defaults)', value: { hex: palette.delft, model: 'hex' } },
];
// #endregion
const TRIM = { w: 160, h: 240 }; // a large pocket guide, in mm
const TOP = 22; // top margin in mm: the cover's container starts here
const OUTER = 22; // outer margin in mm: folios and running heads align to it
const LEAD = 14; // body leading in pt: the baseline grid
const [TEXT, DISPLAY, LABEL] = ['Newsreader', 'Instrument Serif', 'Archivo']; // three voices
// #region answer: every web link of the guide prints as a numbered note with a code to scan
const links = {
print: 'notes', // a note marker after the link text, the address in a note at the foot
show: 'both', // the short address for reading, the code for the phone
urlDisplay: 'short', // metmuseum.org/art/collection/search/436616: no https://, no www.
repeat: 'first', // an address noted once is cited again by its number, never noted twice
qr: { size: mm(16), side: 'start', gap: em(1.2), color: col('ink') },
};
// Link notes are footnotes: they take the note style and the numbering below. 'document'
// numbers them through the walk, so the five stops print notes 3 to 7, not five notes 1.
const footnotes = { placement: 'column', numbering: 'document', fontSize: pt(8),
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
#Print the address in the line instead
For a guide read at home, 'inline' adds the address in parentheses after the link text and sets no note and no code.
- print: 'notes', // a note marker after the link text, the address in a note at the foot
- show: 'both', // the short address for reading, the code for the phone
+ print: 'inline', // (metmuseum.org/…) after the link text#Codes without addresses
show: 'qr' prints the code alone beside the note number, for a poster or a label where an address would only take room.
- show: 'both', // the short address for reading, the code for the phone
+ show: 'qr', // the code alonePitfalls
A note with a code is at least as tall as the code. When the line that carries the link has no room for its note below it on the page, the line moves on with its note, so keep a link out of the last lines of a page that must end where it does, as the cover's first paragraph is here.
Credits
- Recipe
- Ignacio Ferro
- Text
- Original prose, CC BY 4.0
- Images
- Johannes Vermeer, Young Woman with a Water Pitcher, about 1662 (the cover, cropped) · The Metropolitan Museum of Art · CC0 1.0
- Frans Hals, Young Man and Woman in an Inn, 1623 (stop 1) · The Metropolitan Museum of Art · CC0 1.0
- Jan Steen, Merry Company on a Terrace, about 1670 (stop 2) · The Metropolitan Museum of Art · CC0 1.0
- Rembrandt, Aristotle with a Bust of Homer, 1653 (stop 3) · The Metropolitan Museum of Art · CC0 1.0
- Johannes Vermeer, Young Woman with a Water Pitcher, about 1662 (stop 4) · The Metropolitan Museum of Art · CC0 1.0
- Jacob van Ruisdael, Wheat Fields, about 1670 (stop 5) · The Metropolitan Museum of Art · CC0 1.0
- Fonts
- Newsreader (SIL OFL 1.1) · Instrument Serif (SIL OFL 1.1) · Archivo (SIL OFL 1.1)
Edit this write-up ↗ (opens in a new tab)Recipe folder on GitHub ↗ (opens in a new tab)


