Repository navigation
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.
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. |
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. |
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. |
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
placementorgridof 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
shrinkabove 0 give space back, the lowestpriorityfirst; frames of the same priority give in proportion to theirshrink; none goes below itsminHeight. Spare space goes to the frames withgrowabove 0, in proportion to it, up to theirmaxHeight; with none,alignplaces 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 itsminHeightand takes spare space as if it hadgrow: 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.
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,headeroroverflowTemplatereplaces the parent's.gridmerges key by key.spread,gutterandroleare inherited too;id,nameandextendsare the template's own. -
framesandgroupsmerge by name: an entry with the name of an inherited one changes the fields it writes and keeps the rest (fit,overflowandpaddingmerge key by key;placementis replaced whole);remove: truedrops it; a new name is added after the inherited ones. - Decoration elements merge by
idthe 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.
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'.
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.
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. |
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. |
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 →
Postext is MIT-licensed. Maintained by Ignacio Ferro and community contributors. See the live documentation for the most up-to-date reference.
Overview
Writing
- Document Format
- Hyphenation and Justification
- Chinese Layout
- Arabic Layout
- Japanese Layout
- Comics
- Composed Pages
- Sandbox
- Agent Skill
- Command Line
- WebMCP
Configuration
- Overview and index
- Page and layout
- Headers and footers
- Body text
- Hyphenation and language
- Headings
- Widows, orphans and balancing
- Lists and maths
- Notes and references
- East Asian typography
- Resources
- Styles and parts
- Callout boxes
- Comics
- Composed pages
- Fonts, colours, viewers
Code
Internals
External