Pular para o conteúdo principal
Receita número 5

Receitas · Capítulo 4 · Cabeços e fólios

Cabeços pela paridade num livro de ensaios

Cabeços filtrados por paridade e tipo de página: o título do livro nas pares, o do ensaio cortado nas ímpares, fólios em abas na margem e um fólio ao pé.

Nesta página
Saída
Canvas
Postext
Testada com o Postext 1.19.1
Requer ≥ 1.4.1
Licença
Atualizada em 25 de set. de 2026
Código MIT · Texto MIT

p. 30–31 · 2–3 de 5

  • Amostra em inglês: ainda sem edição em português
  • Refile 132 × 198 mm
  • 1 coluna
  • Baskervville 10,2/14
  • Alegreya SC
  • Libre Caslon Display
  • 5 páginas
  • Nível
  • Postext 1.19.1
  • Diagramado em 67 ms
  • 180 linhas de código

Em poucas palavras

Cinco páginas de um livrinho de ensaios. Mostra como imprimir o título do livro no alto das páginas da esquerda e o título do ensaio nas páginas da direita.

O que você vai compor

As páginas 29 a 33 de uma edição de bolso dos Ensaios de Montaigne na tradução inglesa de Charles Cotton: o capítulo IV inteiro e o começo do V. As pares levam o título do livro em maiúsculas espaçadas. As ímpares levam o título do ensaio em itálico, cortado com reticências, já que o do capítulo IV chega a 81 caracteres. Cada fólio fica numa aba sépia na margem externa, a 3 mm de um fio de 0,5 pt na borda da mancha. A abertura não tem cabeço, só um fólio ao pé. O texto vai em Baskervville sobre papel creme; os títulos, em Libre Caslon Display; os fólios e o título do livro, em Alegreya SC. Todos os cabeços são definidos na configuração, então o cabeço embutido do motor, em Open Sans azul de 8 pt, nunca é impresso.

Esta receita responde a

  • Como configuro os cabeços: título do livro na página esquerda, título do capítulo na direita e fólio do lado de fora?
  • Como oculto os cabeços nas aberturas e nas páginas em branco, ou pinto uma página par em branco com a cor da parte?
  • Como evito que títulos, negritos e marcadores de lista saiam em azul?

A resposta curta

script.js · linhas 68–96no código completo
const GAP = 3; // mm from the tab to the divider, which lands on the text block's edge
const HEAD = 12; // mm from the top edge of the page to the top of the tab
const CAP = t({ en: 60.5, es: 66.8 }); // mm: fitted so essay IV's head ends on a word
// (gotcha: ellipsis-mid-word); a short running title (see Variations) suits any title
const page = (edge) => ({ to: 'page', edge }); // the trim box (gotcha: negative-offsets)
const header = { elements: [
  // Verso (even): tab | divider | THE BOOK'S TITLE from the frontmatter (gotcha: quote-frontmatter)
  folio({ id: 'folio-even', parity: 'even', pages: 'body', anchor: page('top-left'),
    x: OUTER - GAP - TAB, y: HEAD }),
  divider({ id: 'rule-even', parity: 'even', from: 'folio-even', edge: 'right-of', x: GAP }),
  { kind: 'text', id: 'book', content: '{title}', parity: 'even', pages: 'body',
    fontFamily: 'Alegreya SC', fontSize: pt(FOLIO_PT), lineHeight: LINE / FOLIO_PT,
    color: col('muted'), textTransform: 'uppercase', letterSpacing: pt(1.3),
    placement: { anchor: { to: '#rule-even', edge: 'right-of' }, // top: the tab's top
      offset: { x: mm(2.5), y: pt(PAD.top) } } }, // PAD.top down: on the folio's baseline
  // Recto (odd): the essay's title, cut short with an ellipsis | divider | tab.
  folio({ id: 'folio-odd', parity: 'odd', pages: 'body', anchor: page('top-right'),
    x: -(OUTER - GAP - TAB), y: HEAD }), // from the right edge, a negative x runs inwards
  divider({ id: 'rule-odd', parity: 'odd', from: 'folio-odd', edge: 'left-of', x: -GAP }),
  { kind: 'text', id: 'essay', content: '{chapterTitle}', parity: 'odd', pages: 'body',
    fontFamily: 'Baskervville', italic: true, fontSize: pt(TITLE_PT),
    lineHeight: LINE / TITLE_PT, color: col('muted'),
    overflow: 'ellipsis-end', // stated, though default (gotcha: overflow-ellipsis-default)
    placement: { anchor: { to: '#rule-odd', edge: 'left-of' },
      offset: { x: mm(-2.5), y: pt(PAD.top) }, size: { maxWidth: mm(CAP) } } },
] };
// Openers: no head, only a drop folio HEAD above the page's foot, centred under the text block.
const footer = { elements: [folio({ id: 'drop-folio', parity: 'all', pages: 'opener',
  anchor: { to: 'container', edge: 'bottom' }, y: -HEAD })] };

Ingredientes

Tipografia
Baskervville, Libre Caslon Display, Alegreya SC (SIL OFL 1.1)
Materiais
Nenhum: todas as imagens são desenhadas em código

Preparo

#1 · Substitua o cabeço azul embutido

script.js · linhas 14–27no código completo
const palette = {
  ink: '#1f1b16', // text: a warm near-black, never #000
  sepia: '#8a5a2b', // the one accent: folio tabs, dividers, opener plates (5.3:1 on paper)
  muted: '#6f6558', // running heads, verse glosses, the colophon (5.2:1 on paper)
  paper: '#f8f4ec', // a warm off-white page; also the type reversed out of the sepia
};
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' } })),
  // Text-style defaults (headings, bold, italic, lists, boxes) link to 'main-color'. The
  // built-in header and footer keep #295AA3, and design slots never read the palette
  // (gotcha: palette-skips-designs), so every element colour here carries its hex.
  { id: 'main-color', name: 'sepia (defaults)', value: { hex: palette.sepia, model: 'hex' } },
];

Cada cor da configuração aponta para uma destas entradas. Se você deixar header sem definir, o motor desenha o cabeço embutido: {title} nas ímpares e {chapterTitle} nas pares, sobre um fio de 1 pt, em Open Sans 600 de 8 pt e no azul #295AA3 (cabeçalhos e rodapés). Apontar main-color para o sépia muda as cores padrão do texto, mas deixa aquele cabeço em azul, porque os espaços de design nunca leem a paleta. Por isso cada cabeço aqui é escrito por inteiro, com as cores em hexadecimal. O negrito, o itálico e as referências também usam main-color por padrão, e bodyText os devolve à cor da tinta.

#2 · Todos os cabeços saem de uma aba

script.js · linhas 32–49no código completo
const FOLIO_PT = 7.5; // pt: the folio, and the book's title in capitals beside it
const TITLE_PT = 8.8; // pt: the essay's title, whose italic lowercase reads small at 7.5
const LINE = FOLIO_PT * 1.2; // pt: the folio and both titles share this line box and baseline
const PAD = { top: 1, bottom: 1.4 }; // pt: 0.4 pt more below for the descending 3, 5, 7, 9
const TAB = 7.5; // mm: the tab's width
const TAB_H = LINE + PAD.top + PAD.bottom; // pt: the tab's height, which the divider matches
const folio = ({ id, parity, pages, anchor, x = 0, y }) => ({
  kind: 'text', id, content: '{pageNumber}', parity, pages, fontFamily: 'Alegreya SC',
  fontSize: pt(FOLIO_PT), fontWeight: 700, lineHeight: LINE / FOLIO_PT, color: col('paper'),
  box: { backgroundColor: col('sepia'), borderRadius: mm(2), // a pill; the number is centred
    padding: { top: pt(PAD.top), bottom: pt(PAD.bottom) } }, // by default in both directions
  placement: { anchor, offset: { x: mm(x), y: mm(y) }, size: { width: mm(TAB) } },
});
const divider = ({ id, parity, from, edge, x }) => ({
  kind: 'rule', id, parity, pages: 'body', direction: 'vertical', thickness: pt(0.5),
  color: col('sepia'), placement: { anchor: { to: `#${from}`, edge }, offset: { x: mm(x) },
    size: { height: pt(TAB_H) } },
});

A resposta curta ancora a aba de cada cabeço em 'page', de modo que os deslocamentos são medidos a partir do refile. A partir do canto top-right, um x positivo sai da página, e por isso a aba da ímpar recebe um valor negativo. O fio pende da aba, e o título, do fio (posicionamento de elementos). As medidas da aba ficam em constantes com nome porque as outras peças são medidas a partir delas. O fio tem TAB_H de altura. O fólio e os dois títulos dividem uma caixa de linha de 9 pt, e a linha de base de um texto de design fica a 80% da altura dessa caixa, então os três ficam sobre a mesma linha de base em qualquer corpo.

Página 30, par: aba, fio e título do livro, da margem externa para dentro.
Página 31, ímpar: o título do ensaio, cortado antes do fio e da aba na margem externa.

#3 · Espelhe as margens e conte linhas inteiras

script.js · linhas 53–64no código completo
const TRIM = { width: 132, height: 198 }; // mm
const TOP = 22; // mm: the top margin, which holds the heads (gotcha: header-paints-over-text)
const INNER = 15; // mm
const OUTER = 21; // mm: the outer margin, where the folio tabs hang
const LINES = 31; // whole lines of LEAD in the text block, so full pages end level
const MEASURE = TRIM.width - INNER - OUTER; // the text block's width: 96 mm
const geometry = { // left is the inner margin on a recto; mirror swaps it on the versos
  width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, // 150 dpi is for the screen
  backgroundColor: col('paper'),
  margins: { top: mm(TOP), bottom: mm(TRIM.height - TOP - (LINES * LEAD * 25.4) / 72),
    left: mm(INNER), right: mm(OUTER), mirror: true },
};

Com mirror: true, a margem right é a externa na página ímpar e passa para a esquerda na par, então OUTER é sempre a margem do corte da frente, onde ficam as abas. A margem inferior sai do refile, da margem superior e de LINES, o que mantém a mancha com exatamente LINES linhas de LEAD (aqui, 31 × 14 pt), qualquer que seja o refile ou a margem superior. Toda página cheia termina na 31ª linha, e por isso páginas que se defrontam, como a 30 e a 31, terminam na mesma altura.

#4 · Deixe a abertura mostrar o título inteiro

script.js · linhas 100–122no código completo
const PLATE = 26; // mm: a square plate at the head of the text block
const opener = {
  enabled: true,
  minHeight: mm(58), // the text of every essay starts on the same line
  slot: { elements: [
    { kind: 'box', id: 'plate', style: { backgroundColor: col('sepia') },
      placement: { anchor: { to: 'container', edge: 'top-left' },
        size: { width: mm(PLATE), height: mm(PLATE) } } },
    { kind: 'text', id: 'numeral', content: '{attr.num}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(48), lineHeight: 1, color: col('paper'),
      placement: { anchor: { to: '#plate', edge: 'align-top' }, // the plate's own box, where
        size: { width: mm(PLATE), height: mm(PLATE) } } }, // the text centres by default
    { kind: 'text', id: 'book', content: '{subtitle}', fontFamily: 'Alegreya SC',
      fontSize: pt(8.5), letterSpacing: pt(1.4), color: col('sepia'), align: 'left',
      placement: { anchor: { to: 'container', edge: 'top-left' },
        offset: { x: mm(PLATE + 4.5), y: mm(PLATE - 2.9) } } }, // its baseline on the plate's foot
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(20), lineHeight: 1.12, color: col('ink'), align: 'left',
      overflow: 'wrap', // an opener shows the whole title; the running head cuts it
      placement: { anchor: { to: '#plate', edge: 'below' }, offset: { y: mm(6) },
        size: { width: mm(MEASURE) } } },
  ] },
};

A abertura compõe o título completo com overflow: 'wrap' e o quebra em cada \\ da linha do título. Só uma abertura de página inteira respeita essas quebras, e daí o span: 'page' do nível (quebras de linha nos títulos). No cabeço, {chapterTitle} sempre dá o título numa linha só, e as reticências cortam essa linha. O numeral vem do atributo {num="IV"} do título, e os versaletes ao lado da placa vêm do subtitle do frontmatter.

#5 · Abra cada ensaio numa página ímpar

script.js · linhas 140–149no código completo
  headings: { // 400, the face's only weight: the default 700 would ask for one that is missing
    fontFamily: 'Libre Caslon Display', fontWeight: 400, color: col('ink'),
    levels: [
      // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
      // 'odd' puts every opener on a recto, adding a blank verso when one is needed;
      // span: 'page' makes the design an opener, where a \\ in the title breaks the line.
      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
        marginTop: pt(0), marginBottom: pt(0), advancedDesign: opener },
    ],
  },

Qualquer objeto headings desliga a quebra de página do H1, por isso ela é declarada de novo. Com 'odd', um ensaio que termina numa ímpar é seguido por uma página par em branco. A edição espanhola tem uma, porque lá o capítulo IV termina na página 31; aqui o capítulo IV termina na página 32, uma par, e o capítulo V abre na 33. O pages: 'body' dos cabeços os mantém fora das páginas em branco e das aberturas (as páginas cujo primeiro bloco é este título). O pages: 'opener' do rodapé imprime o fólio ao pé só nas aberturas (elementos de texto).

#6 · Comece os fólios em 29

script.js · linhas 256–261no código completo
// 28 pages come before this one, so recto and verso, the mirrored margins and the odd/even
// heads follow the book page (gotcha: parity-page1-recto); the folios start at 29.
const continuation = { pageIndexOffset: 28, pageNumbering: { startAt: 29 } };
const doc = await buildWithFonts(
  () => buildDocument({ markdown, continuation }, config()), markdown);
showPages(doc, { title: t({ en: 'Running heads by parity', es: 'Cabeceras según la paridad' }) });

parity segue o lugar da página no livro e ignora o fólio. pageIndexOffset: 28 soma 28 ao índice de cada página, de modo que ímpar e par, as margens espelhadas e os cabeços de cada lado acompanham a página do livro. Depois, pageNumbering.startAt numera estas páginas de 29 a 33. Sem o deslocamento, a primeira página do build é ímpar, seja qual for o fólio que imprime. Com a página 29 isso calha de dar certo, mas com startAt: 30 o título do livro cairia na página 31 e o do ensaio na 32, cada um do lado errado da página dupla.

A receita completa

Sandbox
// ═══ Postext Cookbook · Nº 005 · Running heads by parity ═══════════════════════════════
// https://postext.dev/en/cookbook/running-heads-by-parity
// Code: MIT · Text: Montaigne, tr. Cotton (PD, Gutenberg #3600); es: new translation (MIT)
// Fonts: Baskervville, Libre Caslon Display, Alegreya SC (SIL OFL 1.1) · Needs postext ≥ 1.4.1
// Pages 29 to 33 of a pocket Montaigne. Versos carry the book's title and rectos the essay's,
// the folios sit in tabs in the outer margin, and an opener prints only a folio at the foot.
import { buildDocument, renderPageToCanvas, clearMeasurementCache } from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'running-heads-by-parity';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: four colours with semantic ids; main-color points at the sepia
const palette = {
  ink: '#1f1b16', // text: a warm near-black, never #000
  sepia: '#8a5a2b', // the one accent: folio tabs, dividers, opener plates (5.3:1 on paper)
  muted: '#6f6558', // running heads, verse glosses, the colophon (5.2:1 on paper)
  paper: '#f8f4ec', // a warm off-white page; also the type reversed out of the sepia
};
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' } })),
  // Text-style defaults (headings, bold, italic, lists, boxes) link to 'main-color'. The
  // built-in header and footer keep #295AA3, and design slots never read the palette
  // (gotcha: palette-skips-designs), so every element colour here carries its hex.
  { id: 'main-color', name: 'sepia (defaults)', value: { hex: palette.sepia, model: 'hex' } },
];
// #endregion
const LEAD = 14; // pt: the body leading, the pitch of the baseline grid

// #region tab: the folio tab and its divider, the two pieces every head is built from
const FOLIO_PT = 7.5; // pt: the folio, and the book's title in capitals beside it
const TITLE_PT = 8.8; // pt: the essay's title, whose italic lowercase reads small at 7.5
const LINE = FOLIO_PT * 1.2; // pt: the folio and both titles share this line box and baseline
const PAD = { top: 1, bottom: 1.4 }; // pt: 0.4 pt more below for the descending 3, 5, 7, 9
const TAB = 7.5; // mm: the tab's width
const TAB_H = LINE + PAD.top + PAD.bottom; // pt: the tab's height, which the divider matches
const folio = ({ id, parity, pages, anchor, x = 0, y }) => ({
  kind: 'text', id, content: '{pageNumber}', parity, pages, fontFamily: 'Alegreya SC',
  fontSize: pt(FOLIO_PT), fontWeight: 700, lineHeight: LINE / FOLIO_PT, color: col('paper'),
  box: { backgroundColor: col('sepia'), borderRadius: mm(2), // a pill; the number is centred
    padding: { top: pt(PAD.top), bottom: pt(PAD.bottom) } }, // by default in both directions
  placement: { anchor, offset: { x: mm(x), y: mm(y) }, size: { width: mm(TAB) } },
});
const divider = ({ id, parity, from, edge, x }) => ({
  kind: 'rule', id, parity, pages: 'body', direction: 'vertical', thickness: pt(0.5),
  color: col('sepia'), placement: { anchor: { to: `#${from}`, edge }, offset: { x: mm(x) },
    size: { height: pt(TAB_H) } },
});
// #endregion

// #region page: a pocket trim; the margins mirror, the text block holds 31 whole lines
const TRIM = { width: 132, height: 198 }; // mm
const TOP = 22; // mm: the top margin, which holds the heads (gotcha: header-paints-over-text)
const INNER = 15; // mm
const OUTER = 21; // mm: the outer margin, where the folio tabs hang
const LINES = 31; // whole lines of LEAD in the text block, so full pages end level
const MEASURE = TRIM.width - INNER - OUTER; // the text block's width: 96 mm
const geometry = { // left is the inner margin on a recto; mirror swaps it on the versos
  width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, // 150 dpi is for the screen
  backgroundColor: col('paper'),
  margins: { top: mm(TOP), bottom: mm(TRIM.height - TOP - (LINES * LEAD * 25.4) / 72),
    left: mm(INNER), right: mm(OUTER), mirror: true },
};
// #endregion

// #region answer: running heads by parity: book on the verso, essay on the recto, folios outside
const GAP = 3; // mm from the tab to the divider, which lands on the text block's edge
const HEAD = 12; // mm from the top edge of the page to the top of the tab
const CAP = t({ en: 60.5, es: 66.8 }); // mm: fitted so essay IV's head ends on a word
// (gotcha: ellipsis-mid-word); a short running title (see Variations) suits any title
const page = (edge) => ({ to: 'page', edge }); // the trim box (gotcha: negative-offsets)
const header = { elements: [
  // Verso (even): tab | divider | THE BOOK'S TITLE from the frontmatter (gotcha: quote-frontmatter)
  folio({ id: 'folio-even', parity: 'even', pages: 'body', anchor: page('top-left'),
    x: OUTER - GAP - TAB, y: HEAD }),
  divider({ id: 'rule-even', parity: 'even', from: 'folio-even', edge: 'right-of', x: GAP }),
  { kind: 'text', id: 'book', content: '{title}', parity: 'even', pages: 'body',
    fontFamily: 'Alegreya SC', fontSize: pt(FOLIO_PT), lineHeight: LINE / FOLIO_PT,
    color: col('muted'), textTransform: 'uppercase', letterSpacing: pt(1.3),
    placement: { anchor: { to: '#rule-even', edge: 'right-of' }, // top: the tab's top
      offset: { x: mm(2.5), y: pt(PAD.top) } } }, // PAD.top down: on the folio's baseline
  // Recto (odd): the essay's title, cut short with an ellipsis | divider | tab.
  folio({ id: 'folio-odd', parity: 'odd', pages: 'body', anchor: page('top-right'),
    x: -(OUTER - GAP - TAB), y: HEAD }), // from the right edge, a negative x runs inwards
  divider({ id: 'rule-odd', parity: 'odd', from: 'folio-odd', edge: 'left-of', x: -GAP }),
  { kind: 'text', id: 'essay', content: '{chapterTitle}', parity: 'odd', pages: 'body',
    fontFamily: 'Baskervville', italic: true, fontSize: pt(TITLE_PT),
    lineHeight: LINE / TITLE_PT, color: col('muted'),
    overflow: 'ellipsis-end', // stated, though default (gotcha: overflow-ellipsis-default)
    placement: { anchor: { to: '#rule-odd', edge: 'left-of' },
      offset: { x: mm(-2.5), y: pt(PAD.top) }, size: { maxWidth: mm(CAP) } } },
] };
// Openers: no head, only a drop folio HEAD above the page's foot, centred under the text block.
const footer = { elements: [folio({ id: 'drop-folio', parity: 'all', pages: 'opener',
  anchor: { to: 'container', edge: 'bottom' }, y: -HEAD })] };
// #endregion

// #region opener: the essay's numeral on a sepia plate; the title below it, set in full
const PLATE = 26; // mm: a square plate at the head of the text block
const opener = {
  enabled: true,
  minHeight: mm(58), // the text of every essay starts on the same line
  slot: { elements: [
    { kind: 'box', id: 'plate', style: { backgroundColor: col('sepia') },
      placement: { anchor: { to: 'container', edge: 'top-left' },
        size: { width: mm(PLATE), height: mm(PLATE) } } },
    { kind: 'text', id: 'numeral', content: '{attr.num}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(48), lineHeight: 1, color: col('paper'),
      placement: { anchor: { to: '#plate', edge: 'align-top' }, // the plate's own box, where
        size: { width: mm(PLATE), height: mm(PLATE) } } }, // the text centres by default
    { kind: 'text', id: 'book', content: '{subtitle}', fontFamily: 'Alegreya SC',
      fontSize: pt(8.5), letterSpacing: pt(1.4), color: col('sepia'), align: 'left',
      placement: { anchor: { to: 'container', edge: 'top-left' },
        offset: { x: mm(PLATE + 4.5), y: mm(PLATE - 2.9) } } }, // its baseline on the plate's foot
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(20), lineHeight: 1.12, color: col('ink'), align: 'left',
      overflow: 'wrap', // an opener shows the whole title; the running head cuts it
      placement: { anchor: { to: '#plate', edge: 'below' }, offset: { y: mm(6) },
        size: { width: mm(MEASURE) } } },
  ] },
};
// #endregion

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: t({ en: 'en-us', es: 'es' }), // hyphenation by exact code (gotcha: hyphenation-locales)
  colorPalette,
  page: geometry,
  layout: { layoutType: 'single' },
  bodyText: { // bold, italic and references default to main-color (the sepia): set to the ink
    fontFamily: 'Baskervville', fontSize: pt(10.2), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: mm(4), indentAfterHeading: false,
    // Hyphenation, optimal breaking and widow, orphan and runt control are on by default; a band
    // of 0.7–1.65 (default 0.6–2) evens the grey from line to line. At the default runt length,
    // 20 space widths, English essay IV ends on 'minds.' alone; at 16 it ends on 'our minds.'.
    minWordSpacing: 0.7, maxWordSpacing: 1.65, runtMinCharacters: 16,
  },
  // #region levels: every essay opens on a recto; the heading draws the opener
  headings: { // 400, the face's only weight: the default 700 would ask for one that is missing
    fontFamily: 'Libre Caslon Display', fontWeight: 400, color: col('ink'),
    levels: [
      // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
      // 'odd' puts every opener on a recto, adding a blank verso when one is needed;
      // span: 'page' makes the design an opener, where a \\ in the title breaks the line.
      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
        marginTop: pt(0), marginBottom: pt(0), advancedDesign: opener },
    ],
  },
  // #endregion
  // Montaigne's quotations: a box with no fill keeps verse and gloss together. Its two 10.5 pt
  // margins and a two-line gloss (2 × 10.5 pt) make three whole lines, so the grid adds no space.
  calloutStyles: [{ id: 'quote', backgroundEnabled: false,
    padding: { top: pt(0), right: mm(8), bottom: pt(0), left: mm(8) },
    marginTop: pt(LEAD * 0.75), marginBottom: pt(LEAD * 0.75) }],
  paragraphStyles: [
    { id: 'verse', fontSize: pt(9.6), textAlign: 'center', firstLineIndent: pt(0) },
    { id: 'gloss', fontSize: pt(7.6), lineHeight: pt(LEAD * 0.75), color: col('muted'),
      textAlign: 'center', firstLineIndent: pt(0) },
    // Markdown has no horizontal rule (--- prints as text; gap: markdown-extras), so the rule
    // over the colophon is eight em dashes of Alegreya SC that overlap into a 0.5 pt line.
    { id: 'end', fontFamily: 'Alegreya SC', fontSize: pt(8), color: col('sepia'),
      textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
    // No italics in the colophon: a style's italic runs take bodyText.italicColor, the ink,
    // and would print darker than the muted words around them (gotcha: style-italic-colour).
    { id: 'colophon', fontSize: pt(7), lineHeight: pt(9.5), color: col('muted'),
      textAlign: 'left', firstLineIndent: pt(0) },
  ],
  header,
  footer,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Amostra em Markdown · 70 linhas · content.en.mdtitle: "The Essays of Montaigne" subtitle: "Book the First" author: "Michel de Montaigne" --- # That the soul expends its passions \\ upon false objects, \\ where the true are wanting {num="IV"} A gentleman of my country, marvellously tormented with the gout, being importuned by his physicians totally to abstain from all manner of salt meats, was wont pleasantly to reply, that in the extremity of his fits he must needs have something to quarrel with, and that railing at and cursing, one while the Bologna sausages, and another the dried tongues and the hams, was some mitigation to his pain. But, in good earnest, as the arm when it is advanced to strike, if it miss the blow, and goes by the wind, it pains us; and as also, that, to make a pleasant prospect, the sight should not be lost and dilated in vague air, but have some bound and object to limit and circumscribe it at a reasonable distance. :::callout{type="quote"} :::paragraphs{style="verse"} *Ventus ut amittit vires, nisi robore densae* *Occurrant sylvae, spatio diffusus inani.* ::: :::paragraphs{style="gloss"} As the wind loses its force diffused in void space, unless it in its strength encounters the thick wood. — Lucan, III, 362 ::: ::: So it seems that the soul, being transported and discomposed, turns its violence upon itself, if not supplied with something to oppose it, and therefore always requires an object at which to aim, and whereon to act. Plutarch says of those who are delighted with little dogs and monkeys, that the amorous part that is in us, for want of a legitimate object, rather than lie idle, does after that manner forge and create one false and frivolous. And we see that the soul, in its passions, inclines rather to deceive itself, by creating a false and fantastical subject, even contrary to its own belief, than not to have something to work upon. After this manner brute beasts direct their fury to fall upon the stone or weapon that has hurt them, and with their teeth even execute revenge upon themselves for the injury they have received from another: :::callout{type="quote"} :::paragraphs{style="verse"} *Pannonis haud aliter, post ictum saevior ursa,* *Cui jaculum parva Lybis amentavit habena,* *Se rotat in vulnus, telumque irata receptum* *Impetit, et secum fugientem circuit hastam.* ::: :::paragraphs{style="gloss"} So the she-bear, fiercer for the Libyan’s dart, turns upon the wound and, attacking the spear, twists it as she flies. — Lucan, VI, 220 ::: ::: What causes of the misadventures that befall us do we not invent? what is it that we do not lay the fault to, right or wrong, that we may have something to quarrel with? It is not those beautiful tresses you tear, nor is it the white bosom that in your anger you so unmercifully beat, that with an unlucky bullet have slain your beloved brother; quarrel with something else. Livy, speaking of the Roman army in Spain, says that for the loss of the two brothers, their great captains, *flere omnes repente, et offensare capita*, “all at once wept and tore their hair.” ’Tis a common practice. And the philosopher Bion said pleasantly of the king, who by handfuls pulled his hair off his head for sorrow, “Does this man think that baldness is a remedy for grief?” Who has not seen peevish gamesters chew and swallow the cards, and swallow the dice, in revenge for the loss of their money? Xerxes whipped the sea, and wrote a challenge to Mount Athos; Cyrus employed a whole army several days at work, to revenge himself of the river Gyndas, for the fright it had put him into in passing over it; and Caligula demolished a very beautiful palace for the pleasure his mother had once enjoyed there. I remember there was a story current, when I was a boy, that one of our neighbouring kings, having received a blow from the hand of God, swore he would be revenged, and in order to it, made proclamation that for ten years to come no one should pray to Him, or so much as mention Him throughout his dominions, or, so far as his authority went, believe in Him; by which they meant to paint not so much the folly as the vainglory of the nation of which this tale was told. They are vices that always go together, but in truth such actions as these have in them still more of presumption than want of wit. Augustus Caesar, having been tossed with a tempest at sea, fell to defying Neptune, and in the pomp of the Circensian games, to be revenged, deposed his statue from the place it had amongst the other deities. Wherein he was still less excusable than the former, and less than he was afterwards when, having lost a battle under Quintilius Varus in Germany, in rage and despair he went running his head against the wall, crying out, “O Varus! give me back my legions!” for these exceed all folly, forasmuch as impiety is joined therewith, invading God Himself, or at least Fortune, as if she had ears that were subject to our batteries; like the Thracians, who when it thunders or lightens, fall to shooting against heaven with Titanian vengeance, as if by flights of arrows they intended to bring God to reason. Though the ancient poet in Plutarch tells us: :::callout{type="quote"} :::paragraphs{style="verse"} *Point ne se faut couroucer aux affaires,* *Il ne leur chault de toutes nos choleres.* ::: :::paragraphs{style="gloss"} We must not trouble the gods with our affairs; they take no heed of our angers and disputes. — Plutarch ::: ::: But we can never enough decry the disorderly sallies of our minds. # Whether the governor of a place \\ besieged ought himself \\ to go out to parley {num="V"} Quintus Marcius, the Roman legate in the war against Perseus, King of Macedon, to gain time wherein to reinforce his army, set on foot some overtures of accommodation, with which the king being lulled asleep, concluded a truce for some days, by this means giving his enemy opportunity and leisure to recruit his forces, which was afterwards the occasion of the king’s final ruin. Yet the elder senators, mindful of their forefathers’ manners, condemned this proceeding as degenerating from their ancient practice, which, they said, was to fight by valour, and not by artifice, surprises, and night-encounters; neither by pretended flight nor unexpected rallies to overcome their enemies; never making war till having first proclaimed it, and very often assigned both the hour and place of battle. :::paragraphs{style="end"} ———————— ::: :::paragraphs{style="colophon"} Chapters IV and V of Book the First of Montaigne’s essays, in Charles Cotton’s translation as revised by W. C. Hazlitt (1877), Project Gutenberg eBook #3600; notes shortened or omitted, misprints corrected. Set in Baskervville, Libre Caslon Display and Alegreya SC (SIL Open Font License). :::
`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses, loaded before the first build (gotcha: fonts-first). const FONTS = { // text, display and label faces (Libre Caslon Display has no italic) Baskervville: ['400', '400i'], 'Libre Caslon Display': ['400'], 'Alegreya SC': ['400', '700'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); // #region build: pages 29 to 33 of the book // 28 pages come before this one, so recto and verso, the mirrored margins and the odd/even // heads follow the book page (gotcha: parity-page1-recto); the folios start at 29. const continuation = { pageIndexOffset: 28, pageNumbering: { startAt: 29 } }; const doc = await buildWithFonts( () => buildDocument({ markdown, continuation }, config()), markdown); showPages(doc, { title: t({ en: 'Running heads by parity', es: 'Cabeceras según la paridad' }) }); // #endregion
Kit · core, fonts, viewer: igual em todas as receitas · 236 linhas// ─── 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 v2 ── the same in every recipe · postext.dev/cookbook // Postext measures with the loaded faces and caches the widths: load every face // before the first build, from Fontsource, the files the PDF embeds too. /** 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. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { 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', }; const jobs = []; let added = 0; 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 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 = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` and loads any face the pages use that FONTS missed (a regular * one with a warning), then clears the measurement cache and builds again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout; `base` marks a block's own face. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** 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, '-'); } /** 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 v1 ── 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. */ function showPages(docs, { title, width = 460 } = {}) { 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, { 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 ───────────────────────────────────────────────────────────────────────

O script.js montado funciona como está: cole-o como script de módulo em qualquer página ou abra a receita no CodePen. Pasta da receita no GitHub ↗ (abre em uma nova aba)

Variações

#Dê aos títulos longos um título corrente curto

Escreva o título corrente na linha do título, {num="IV" short="Of false objects"}, e imprima o atributo no lugar do título inteiro; um capítulo sem short não imprime nada ali.

-  { kind: 'text', id: 'essay', content: '{chapterTitle}', parity: 'odd', pages: 'body',
+  { kind: 'text', id: 'essay', content: '{attr.short}', parity: 'odd', pages: 'body',

#Deixe os ensaios abrirem em qualquer lado

Com 'any', o ensaio começa na página seguinte, par ou ímpar, e nunca se acrescenta uma página em branco. A placa pende do canto superior esquerdo da mancha e o fólio ao pé fica centralizado embaixo dela, então os dois ficam do mesmo jeito numa par e numa ímpar.

-      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
+      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'any' },

#Pinte a página par em branco com a cor de destaque

Uma caixa com pages: 'blank' só é impressa nas páginas que a quebra para ímpar acrescenta: na edição espanhola ela enche a página 32 de sépia, e a edição inglesa, que não tem página em branco, não muda.

 const footer = { elements: [folio({ id: 'drop-folio', parity: 'all', pages: 'opener',
-  anchor: { to: 'container', edge: 'bottom' }, y: -HEAD })] };
+  anchor: { to: 'container', edge: 'bottom' }, y: -HEAD }),
+  { kind: 'box', id: 'blank-leaf', pages: 'blank', style: { backgroundColor: col('sepia') },
+    placement: { anchor: page('top-left'),
+      size: { width: mm(TRIM.width), height: mm(TRIM.height) } } }] };

Erros comuns

Erro comum

As reticências cortam no meio da palavra e mantêm o espaço anterior

No postext 1.4.1, overflow: 'ellipsis-end' corta depois do último caractere que cabe, não num limite de palavra, e mantém o espaço se o corte cair logo depois dele, então um cabeço pode terminar em “fals…” ou “false …”. Ajuste maxWidth ao fim de uma palavra quando você conhece o título, ou dê aos títulos longos um título curto de cabeço num atributo do título. Textos, fios e caixas nos designs de página →

Erro comum

O excesso de texto de design é 'ellipsis-end' por padrão

Um elemento de texto de design que não cabe na sua largura termina em reticências por padrão. Use overflow: 'wrap' nos títulos que devem passar para mais linhas. Textos, fios e caixas nos designs de página →

Erro comum

Deslocamentos negativos relativos ao contêiner não pintam nada

O texto de design com largura automática fica limitado ao seu contêiner, então um deslocamento negativo em relação ao contêiner o empurra para fora e nada é pintado. Ancore esses elementos na página ou na sangria com deslocamentos explícitos em mm, ou dê a eles uma largura fixa. Ancoragem de elementos de design →

Erro comum

Uma paleta trocada não chega aos elementos de design nem à cor das referências

postext 1.4.1 aplica colorPalette aos estilos de texto (corpo, títulos, listas, legendas, tabelas, boxes), mas não aos elementos de cabeçalhos, rodapés, aberturas e páginas de parte, nem a bodyText.referenceColor: eles mantêm o hex escrito ao lado do seu paletteId. Se você trocar a paleta, para uma edição de tela escura ou para mudar as cores, reescreva cada cor vinculada a partir de colorPalette antes de compor. Paleta de cores semântica →

Erro comum

Os elementos de cabeçalho e rodapé são pintados sobre o texto

Os elementos de cabeçalho e rodapé são pintados por cima da página, e a área de texto não abre espaço para eles. Mantenha esses elementos dentro das margens, que são o que reserva o espaço deles. Cabeços e fólios →

Erro comum

A página 1 é ímpar: planeje com os números físicos

A página 1 fica à direita e a 2 é a primeira página par, então planeje as páginas duplas com os números físicos de página: uma abertura em página par fica de frente para a ímpar que vem depois dela. Quebras de página e de coluna →

Erro comum

Qualquer objeto headings desativa a quebra de página do H1

Por padrão, um H1 salta para uma página ímpar (always-odd), mas passar qualquer objeto headings redefine esse padrão, então os capítulos ficam emendados e span: 'page' não faz nada. Declare de novo headings.levels[0].breakBefore: { enabled: true, parity } em toda configuração. Capítulos que abrem em página ímpar →

Erro comum

Coloque entre aspas cada valor do frontmatter

O YAML lê title: 1984 como número e uma data como objeto Date, e valores que não são strings saem vazios nos placeholders e deixam o PDF sem título. Coloque cada valor entre aspas: title: "1984". Metadados do documento →

Erro comum

Carregue todas as fontes antes do layout

O motor de layout mede o texto com as fontes que o navegador carregou e guarda as larguras em cache, então uma fonte que chega depois da primeira composição deixa quebras de linha erradas e um PDF que não corresponde mais à tela. Carregue antes todos os pesos e estilos e chame clearMeasurementCache() antes de recompor quando alguma chegar atrasada. Fontes antes da diagramação →

Erro comum

Um estilo de parágrafo não tem cor de itálico

No postext 1.4.1, um estilo de parágrafo define color e boldColor, mas não italicColor: os trechos em itálico usam bodyText.italicColor. Um estilo atenuado (letra miúda, a linha “Fonte:” de uma tabela) imprime os títulos em itálico mais escuros que o texto em volta. Mantenha esses estilos na cor do texto corrido ou evite itálico neles. Estilos de parágrafo →

Erro comum

Só 8 idiomas têm hifenização, com o código exato

A hifenização existe para en-us, es, fr, de, it, pt, ca e nl, com o código exato: 'es-ES' ou qualquer outro idioma passa sem aviso para o inglês americano. Hifenização e idioma do documento →

Verificação do Sandbox · headerFooterUnknownPlaceholder

Marcador desconhecido

Por quê. Um texto de design usa um marcador {name} que não existe, então ele é impresso literalmente.

Correção. Use um marcador aceito ({pageNumber}, {chapterTitle}, {title}, {attr.x}…) ou escreva {{ e }} para chaves literais. Docs →

Verificação do Sandbox · headerFooterMetadataMissing

Metadado do marcador ausente

Por quê. Um marcador como {author} ou {title} não tem valor nos metadados do documento, então é impresso vazio.

Correção. Acrescente o valor ao frontmatter, entre aspas, ou remova o marcador. Docs →

  • CAP é ajustado ao título do ensaio IV em cada edição, o único que um cabeço mostra aqui. O cabeço do ensaio V ainda seria cortado no meio de uma palavra (“Whether the governor of a place besieged ou…”), então um livro de títulos longos precisa do título corrente curto das Variações.
  • Na edição inglesa, subir minWordSpacing de 0,7 para 0,75 deixa a primeira linha do ensaio V (“Quintus Marcius, the Roman legate…”) sem nenhum ponto de quebra dentro da faixa, e os espaços entre palavras se abrem até 2,4 vezes a largura normal. A edição espanhola não tem essa linha. Depois de qualquer mudança, confira as linhas mais frouxas que a captura lista.

Créditos

Texto
Fontes
Baskervville (SIL OFL 1.1) · Libre Caslon Display (SIL OFL 1.1) · Alegreya SC (SIL OFL 1.1)
Sandbox