Skip to content

Configuration Composed Pages

Ignacio Ferro Picón edited this page Oct 16, 2026 · 1 revision

Configuration: composed pages

Part of the Configuration reference.

Generated from docs/configuration-composed-pages-en.mdx (updated 2026-10-16). Read it on postext.dev, where it is also available in Spanish, Catalan, Portuguese, French, German, Italian, Chinese, Japanese and Arabic. To change this page, edit the MDX and run pnpm wiki in apps/web.

Page templates

A composed page is a page whose form comes from a template in the configuration and whose content comes from the Markdown of a ::::page{template="…"} fence. The template names rectangles, called frames; the Markdown sends each piece of content to a frame by its name (its slot). Geometry never appears in the Markdown, and content text never appears in the template. The pageTemplates property holds the templates of the book:

const config: PostextConfig = {
  calloutStyles: [{ id: 'caja-azul' }, { id: 'tab-titulo' }],
  pageTemplates: [
    {
      id: 'tema-cajas',
      decoration: {
        elements: [
          { kind: 'image', id: 'fondo', resourceId: '{attr.fondo}', decorative: true,
            placement: { anchor: { to: 'bleed', edge: 'top-left' }, size: { width: 'fill', height: 'fill' } } },
        ],
      },
      frames: [
        { name: 'titulo', takes: 'h1', style: 'tab-titulo' },
        { name: 'texto', flow: true },
        { name: 'dinero', style: 'caja-azul', padding: { right: { value: 62, unit: 'mm' } } },
        { name: 'dinero-fotos', layer: 2, width: { value: 60, unit: 'mm' },
          placement: { anchor: { to: '#dinero', edge: 'top-right' } } },
      ],
      groups: [
        { name: 'pagina', kind: 'stack', gap: { value: 6, unit: 'mm' }, frames: ['titulo', 'texto', 'dinero'] },
      ],
    },
  ],
};

A template that sets nothing but its id and one frame is a whole page with that frame filling the content area. Every other field is optional:

Property Type Default Description
id string required The name a page fence asks for (template="tema-cajas"). Unique among the templates: a second one with the same id is ignored.
name string the id The name shown in the Sandbox. Layout never reads it.
extends string none The id of a template this one starts from. See Inheritance and variants.
area 'content' | 'trim' | 'bleed' 'content' The rectangle frame positions refer to. 'content' is the page's content area, mirrored on left and right pages like the running text; 'trim' is the trimmed page; 'bleed' the page with its bleed.
grid { columns, rows, columnGap?, rowGap? } none Divides the area into cells, so a frame or a group can be placed by cell (grid: { col: 2, row: 1, cols: 2 }, 1-based) instead of by placement. The gaps default to 0.
frames FrameConfig[] [] The frames, in template order: the order measured first and the one that breaks ties. See Frames.
groups FrameGroupConfig[] [] Stacks and rows of frames that share space. See Groups.
decoration DesignSlot none Images, rules and boxes painted with the page, in the element language of headers and footers. Text elements hold placeholders only ({pageNumber}, {chapterTitle}, {attr.x} for an attribute of the page fence). An element may anchor to a frame or a group ('#dinero') and then moves with it. Decoration is laid after the frames and never pushes text.
header, footer DesignSlot | false inherited The running head and the folio of the page. Unset, the page keeps those of its chapter; false hides them.
spread boolean false The template describes two facing pages laid as one sheet (see Composed pages › Spreads). A spread starts on a verso, the left page of a left-to-right book and the right page of a right-to-left one, with a blank page before it when needed. Its page fence carries the spread flag too; when they disagree the template decides (spreadMismatch).
gutter Dimension 0 On a spread, a band of this width centred on the spine that frames keep off unless they set crossGutter: true: a frame reaching across it is cut at its edge on the side of its centre, one starting inside it moves out. With 0 the band is the spine itself.
variants { recto?, verso? } none Changes for a right page (recto) or a left page (verso), written like the template itself. The side is the physical one, so left-to-right and right-to-left books both work.
overflowTemplate string this template The template of the pages added when a frame continues on the next page (overflow.continueIn: 'next-page'). Unset, the added page uses this template with only the continued frames filled.
bodyStyle string none A paragraph style for the text of the page, as heading styles have.
palette string none A palette override for the page, as heading styles and parts have.
role string 'composed' The page role that design elements with a pages filter see.

Frames

A frame is a named rectangle that receives typeset content. Its name is the slot the Markdown asks for, and it is unique in its template (a group cannot take the name of a frame either, since anchors name both the same way).

A frame is placed by its grid cell when the template has a grid, else by its placement, which works like a design element's: an anchor ('container' is the area, 'page', 'bleed', or '#name' for another frame or group), an edge, an offset and a size. A frame anchored to another one moves with it. Without either, the frame starts at the top-left corner of the area, as wide as the area, and as tall as its height rule says. How the content is poured into the frames, chained and clipped is described in Composed pages › How the page is set.

Property Type Default Description
name string required The slot name.
placement ElementPlacement the whole area Anchor, edge, offset and size, as in element placement. Ignored when a grid cell places the frame.
grid { col, row, cols?, rows? } none A cell of the template grid, 1-based, spanning cols × rows cells (default 1). A cell past the grid is moved back inside it. Ignored when the template has no grid.
width, height Dimension | { percent } | 'auto' | 'fill' 'fill', 'auto' A fixed length, a percentage of the area (0 to 100), 'auto' (the content's own size) or 'fill' (what is left up to the edge the frame is anchored to). Unset, the frame reads placement.size, then the default: full width, as tall as its content. A text frame needs a measure, so 'auto' width only makes sense for a frame that holds one picture (the picture's width at the frame's height). A value the engine cannot use (a negative length, an unknown unit) reads as the default.
minWidth, maxWidth, minHeight, maxHeight Dimension none Limits for an 'auto' or 'fill' size and for the space a group gives or takes. The minimum wins over the maximum, and a fixed size outside them is brought between them (frameMinMax warns of both).
style string frameless A callout style id: frames are drawn with callout styles (background, border, padding, stripe, icon, label tab, title). The words of a tab come from the Markdown (:::frame{slot="dinero" label="El dinero"}). The callout settings about where a box goes in the running text (placement, span, columns, width, fixed…) do not apply to a frame.
padding { top?, right?, bottom?, left? } the style's Overrides the style's padding side by side: room for the photos that sit over a box, for example.
layer number 1 Paint order. Decoration is layer 0; a negative layer paints below it.
rotate number 0 Degrees, clockwise. The box and its content turn together.
verticalAlign 'top' | 'middle' | 'bottom' | 'justify' 'top' Where the content sits in a frame taller than it.
columns, columnGap number, Dimension 1, the style's Columns inside the frame.
exclude 'none' | 'push' 'none' 'push': the text of other frames wraps around this one's rectangle.
flow boolean false The frame receives the content that names no slot.
takes 'h1' … 'h6' none The frame receives the first heading of that level that names no slot, so a page title is just # Title at the top of the fence.
next string none A chain: content that does not fit continues in the frame of that name, on the same page.
overflow.continueIn string none Where content that does not fit goes on: '#frame' (another frame of the page, as next), 'next-page' (the same frame on a page added after this one) or 'template:<id>' (a page of that template).
overflow.mark string | false the language's The mark at the end of a cut piece. Unset, the continuation word of the document language (as split tables use); false sets none.
fit.shrinkText boolean true When nothing else makes the content fit, its text may be set smaller.
fit.minScale number 0.9 The smallest text scale, above 0 and at most 1 (a larger value reads as 1).
fit.scaleImages, fit.imageMinScale boolean, number true, 0.8 Whether the pictures in the frame may be set smaller, and down to what scale.
pin boolean false The frame never moves or resizes, for a box that must stay aligned with the artwork. A group flows around it.
grow, shrink, priority number 0 How the frame shares the space of its group: grow takes a share of the spare space, shrink lets the frame give space back, and frames with a lower priority give it first.
optional boolean false No warning when the Markdown sends nothing to the frame.
crossGutter boolean false On a spread, the frame may run across the spine band. Its text is read on the page that holds its centre; the facing page paints its half.
writingMode, direction 'horizontal-tb' | 'vertical-rl', 'ltr' | 'rtl' the page's A vertical box on a horizontal page, or the other way round.
remove boolean false In a template that extends another, or in a variant: drop the inherited frame of that name.

Groups

A group is a stack (top to bottom) or a row (start to end) of frames that share space and move together. Inside a group, the frames' own position along the group's direction is ignored: the group lays them out in order, with gap between them. When the text of one frame grows, the frames after it move along. A group is a box that frames and decoration can anchor to ('#pagina').

Property Type Default Description
name string required Unique among the frames and groups of the template.
kind 'stack' | 'row' 'stack' Top to bottom, or start to end.
frames string[] [] The frames of the group, in their order on the page. A frame belongs to one group at most; a name that is no frame of the template, or a frame already listed, is skipped.
gap Dimension 0 The room between two frames of the group.
placement, grid as frames its frames' box The group's own box. Unset, the group starts where its first frame is placed and runs as far as its frames need.
align 'start' | 'center' | 'end' | 'space-between' 'start' Where the spare space goes when no frame of the group grows.
remove boolean false In a template that extends another, or in a variant: drop the inherited group.

How groups share space

A frame declares its size: a fixed height, or 'auto', the height of what it holds, kept between minHeight and maxHeight. For a template drawn from a printed page, give each box the height it has on the page as minHeight: a short text keeps the drawn box, a longer one makes it taller. The page is laid twice in the same pass, once as the template declares it and once as its content needs, and what differs between the two is the reflow log.

Stack. The frames of a stack go one under another at their heights, gap between them. Each keeps its own horizontal place and width; its own vertical place is ignored.

  • A group with no placement or grid of its own starts where the placement of its first frame puts it and runs down as far as its frames need, to the foot of the template's area. When one frame grows, the frames after it move down by as much.
  • A group with a box of its own lays its frames inside that box. When they need more than it holds, the frames with shrink above 0 give space back, the lowest priority first; frames of the same priority give in proportion to their shrink; none goes below its minHeight. Spare space goes to the frames with grow above 0, in proportion to it, up to their maxHeight; with none, align places the frames ('end' at the foot, 'center', 'space-between'). A frame of the group with no placement of its own takes the group's box as its area.
  • When nothing more can give, no frame runs past the group's foot: the content that does not fit is cut there and reported as frameOverflow.
  • A frame with height: 'fill' asks for its minHeight and takes spare space as if it had grow: 1.

Row. The frames of a row go side by side at their declared widths, all as tall as the row: the height of the group's box, or, with no box of its own, the tallest declared height among them (a frame with a fixed height keeps its own). When the text of one frame runs past the row's height, the frame takes width: first the spare width of the row, then width from siblings with shrink above 0, the lowest priority first, each one never narrower than the width its own text still fits in. The width taken is the fewest whole millimetres that make the text fit; if even all the room there is would not, the frame takes none, and a row with no box of its own grows taller instead. Spare width goes to the frames with grow above 0, then to align. Rows run from left to right.

Pins. A frame with pin: true keeps the box the template declares for it, even when the frame it is anchored to grows or moves: a box drawn to match the artwork stays on it. In a group, a pinned frame holds its place and the other frames go past it, gap kept on both sides; a pinned frame outside the group does the same for the group's frames it would overlap.

Decoration. A decoration element anchored to a frame (anchor: { to: '#dinero' }) moves with the frame when the frame grows or moves. An element with pin: true stays where the template draws that frame.

Property Type Default Description
pin (decoration element) boolean false On a composed page's decoration: an element anchored to a frame stays where the template puts that frame, whatever the frame's content or group does. Ignored on headers, footers and heading designs.

The three boxes of a textbook page, each at the height it is drawn and growing with its text:

frames: [
  { name: 'dinero', style: 'caja-azul', width: { value: 186, unit: 'mm' }, minHeight: { value: 36, unit: 'mm' },
    placement: { anchor: { to: '#texto', edge: 'below' }, offset: { y: { value: 9, unit: 'mm' } } } },
  { name: 'dinero-fotos', layer: 2, width: { value: 58, unit: 'mm' },
    placement: { anchor: { to: '#dinero', edge: 'top-right' } } },
  { name: 'gasto', style: 'caja-verde', width: { value: 186, unit: 'mm' }, minHeight: { value: 62, unit: 'mm' } },
  { name: 'ahorro', style: 'caja-amarilla', width: { value: 186, unit: 'mm' }, minHeight: { value: 30, unit: 'mm' } },
],
groups: [{ name: 'cajas', kind: 'stack', frames: ['dinero', 'gasto', 'ahorro'], gap: { value: 12, unit: 'mm' } }],

The result depends only on the page's content, the template and the typography: the same input gives the same page, measured in the same order with the same steps. The negotiation never reads another page, so a change on one composed page leaves every other page as it was, as long as the content stays on its page.

Inheritance and variants

A template with extends starts from the template it names (which may itself extend another) and changes only what it writes:

  • A field such as area, header or overflowTemplate replaces the parent's. grid merges key by key. spread, gutter and role are inherited too; id, name and extends are the template's own.
  • frames and groups merge by name: an entry with the name of an inherited one changes the fields it writes and keeps the rest (fit, overflow and padding merge key by key; placement is replaced whole); remove: true drops it; a new name is added after the inherited ones.
  • Decoration elements merge by id the same way; an element with a new id, or none, is added.

variants.recto and variants.verso are written like a template and merge the same way, on a right or a left page. They apply after the whole extends chain, the root's variant first. A spread has both sides, so no variant applies to it.

pageTemplates: [
  { id: 'tema', frames: [{ name: 'texto', flow: true }, { name: 'margen', width: { value: 30, unit: 'mm' },
      placement: { anchor: { to: 'container', edge: 'top-left' } } }],
    variants: { recto: { frames: [{ name: 'margen', placement: { anchor: { to: 'container', edge: 'top-right' } } }] } } },
  { id: 'tema-sin-margen', extends: 'tema', frames: [{ name: 'margen', remove: true }] },
]

A value equal to a default is never dropped when the Sandbox saves a template: in a template that extends another, or in a variant, it overrides the parent's value.

From code, resolvePageTemplate(config, id, { parity }) returns a template with its chain merged, the variant of the side applied, every default filled in and every reference settled as the warnings below describe; validatePageTemplates(config) returns the warnings.

What the layout applies

The layout applies the placement and sizes of every frame (grid, anchors, min/max, verticalAlign), its style and padding, columns, layer, rotate, exclude, flow, takes, next and overflow.continueIn: "#frame", and the template's area, grid, decoration, header, footer and variants. Read and checked, but not yet applied: a group's negotiation of space (grow, shrink, priority, pin, and a group's gap and align; its frames keep their own placement), fit, overflow.continueIn: "next-page" and overflowTemplate, a frame's writingMode and direction, and the template's bodyStyle and palette. An 'auto' width reads as 'fill'.

Spreads

On a spread template every coordinate runs over the sheet of its two pages, with the spine in the middle: area, grid, anchors to container, page and bleed, and fill sizes all read the double width. bleed reaches the head, the foot and both outer edges, never the spine. A textbook's double-page timeline is one template:

pageTemplates: [{
  id: 'linea-tiempo',
  spread: true,
  gutter: { value: 10, unit: 'mm' },
  grid: { columns: 6, rows: 12, columnGap: { value: 12, unit: 'mm' }, rowGap: { value: 3, unit: 'mm' } },
  frames: [
    { name: 'titulo', takes: 'h1', style: 'tab-titulo' },
    { name: 'texto', flow: true, width: { value: 196, unit: 'mm' },
      placement: { anchor: { to: '#titulo', edge: 'below' } } },
    { name: 'recuadro', style: 'recuadro', grid: { col: 4, row: 1, cols: 3, rows: 4 }, height: 'auto' },
    { name: 'banda-1', style: 'banda', grid: { col: 1, row: 5, cols: 3 }, height: { value: 8, unit: 'mm' } },
    { name: 'banda-2', style: 'banda', crossGutter: true, width: 'fill', height: { value: 8, unit: 'mm' },
      placement: { anchor: { to: '#banda-1', edge: 'right-of' }, offset: { x: { value: 2, unit: 'mm' } } } },
    { name: 'era-1', style: 'era', grid: { col: 1, row: 6, rows: 2 }, height: 'auto' },
    // … era-2 to era-6, one grid column each, and the notes under the strip
    { name: 'tira', crossGutter: true, grid: { col: 1, row: 8, cols: 6, rows: 3 } },
  ],
}]

A column gap wider than the gutter keeps the cards of a symmetric grid clear of the spine, so none of them is moved. The second band and the picture strip set crossGutter to run across it: each is read on the page that holds its centre and painted on both. Every word of the page, the era names on the tabs included, stays in the Markdown.

Label styles

The labelStyles property names the looks of the labels set on a picture: the seas and towns of a map, the parts of a diagram. A label stands at an anchor of the picture (a point stored with the resource, as comic speakers are) and takes the style the anchor or the label names.

labelStyles: [
  { id: 'mar', fontStyle: 'italic', textTransform: 'uppercase', letterSpacing: 0.12,
    color: { hex: '#2b6ca3', model: 'hex' } },
  { id: 'ciudad', fontWeight: 600, halo: { color: { hex: '#ffffff', model: 'hex' }, width: { value: 0.6, unit: 'pt' } } },
]
Property Type Default Description
id string required The name anchors and labels use.
name string the id The name shown in the Sandbox.
paragraphStyle string none A paragraph style whose type the label borrows; the fields below override it.
fontFamily, fontSize, fontWeight string, Dimension, number the body text's The type of the label.
fontStyle 'normal' | 'italic' 'normal' Upright or italic.
color ColorValue the body text's The colour of the letters.
textTransform 'none' | 'uppercase' 'none' Capitals, as maps set seas and countries.
letterSpacing number 0 Extra space between letters, in ems.
halo { color, width } none An outline behind the letters, so they read over a busy picture.
background, padding ColorValue, Dimension none A plate behind the label, and the room around its text.
minScale number 0.85 How small a label may be set when it does not fit its width.

Label fields of an anchor

A picture's anchors (Resource.anchors, the points comics use for speakers) take a few optional fields for labels. They are fractions of the picture, so they hold in every language and through any crop of the picture. Comics ignore them.

Field Type Default Description
width number the label's own The measure of the label, as a fraction of the picture's width: a long name breaks onto two lines inside it.
align 'start' | 'center' | 'end' 'center' Which side of the label stands at the anchor point.
verticalAlign 'top' | 'middle' | 'bottom' 'middle' Which edge of the label stands at the anchor's height.
rotate number 0 Degrees, clockwise, around the anchor point: a river's name along its course.
style string none A labelStyles id. A label that names its own style overrides it.

Page template warnings

Elsewhere the engine ignores a key it does not know. Page templates and label styles are checked instead, and every problem is a configuration warning in postext check and in the Sandbox's Checks panel. The path of these warnings is a JSON pointer to the field at fault: /pageTemplates/3/frames/2/style, /pageTemplates/0/variants/recto/frames/1/next, /resources/4/anchors/0/style. When the field was inherited, the pointer names the template that wrote it. A warning that names an id or a name that does not exist also suggests the closest one that does.

A misspelt key of a template, frame, group, grid, fit, overflow, variant or label style is an unknownConfigKey; a word outside a setting's choices (area, verticalAlign, exclude, takes, kind, align, writingMode, direction, fontStyle, textTransform) is an unknownConfigValue, and the setting keeps its default. A template with no id, or a frame or group with no name, is ignored with an unknownConfigValue at that field.

Warning Cause Fix
templateDuplicate Two templates have the same id. The first one is used and the later one is ignored. Rename one of them, or delete the copy.
templateExtendsCycle Following extends comes back to the same template (a → b → a). Every template of the loop, and every template that extends one of them, is dropped: no page can use it. Remove the extends of one template of the loop.
templateExtendsUnknown extends names an id no template has. The template is used on its own, with nothing inherited. Correct the id (the warning suggests the closest one), or add the parent template.
frameDuplicate Two frames, or two groups, of one list have the same name, or a group has the name of a frame. The later entry is ignored; an anchor to that name reads the frame. Give each frame and group a name of its own. To change an inherited frame, write one entry with its name.
frameStyleUnknown A frame's style names no callout style. The frame is drawn without a box. Correct the id, or add the style to calloutStyles.
frameAnchorUnknown A frame, a group or a decoration element is anchored to '#name' and the template has no frame, group or element of that name; or frames and groups are anchored to each other in a loop (also through a group anchored to nothing of its own, whose box is that of its frames). The anchor reads as 'container', the area. Anchor to a frame or group that exists, or break the loop by anchoring one of them to the area.
frameMinMax A minimum is larger than its maximum (minHeight 40mm > maxHeight 30mm), or a fixed width or height lies outside them. The minimum wins, a fixed size is brought between the limits, and used says what the frame takes. Lengths in different units are compared at the page's resolution, ems at the body text size. Make the minimum no larger than the maximum, or drop the limit the fixed size contradicts.
groupFrameUnknown A group's frames lists a name that is no frame of the template (perhaps removed by a child template or a variant). The group skips it. Correct the name, or remove it from the list.
groupFrameTwice A frame is listed in two groups, or twice in one. A frame belongs to one group only: the later listing is skipped. Keep the frame in the one group it moves with.
frameChainLoop Frames continue into each other in a loop through next or overflow.continueIn: '#frame' (a → b → a). The link that closes the loop is dropped, so the chain ends at the frame before it. End the chain at its last frame: remove its next, or make it continue on the next page.
overflowTargetUnknown A next names no frame of the template; an overflow.continueIn names no frame ('#x') or no template ('template:x'), or is in none of the forms '#frame', 'next-page' and 'template:<id>'; or overflowTemplate names no template. Content that does not fit does not continue there. Correct the name or the id, or write one of the three forms.
labelStyleUnknown An anchor of a picture names a style that labelStyles does not have. The warning names the resource. The labels at that anchor take the default look. Correct the style id on the anchor, or add the style to labelStyles.

The configuration version is 15 since page templates (bundles written before keep loading unchanged: nothing an older configuration says changes meaning).


← Configuration: comics · Configuration: units, fonts, colours, viewers and debugging →

Clone this wiki locally