Templates allow editors to centrally control content and reuse content. They allow a developer to not have to hard code layout decisions and instead use rules to apply user layouts in template content stored separately from the page, or give the user a choice on which layout they want.
Templates:
allowedTemplates and allowedLayouts applied to the blocks schema let the developer control loading templates: which templates are available for use, which are automatically applied as layouts, and which the editor can switch betweenexpandTemplates / expandTemplatesSync) to refresh templates found in the page content from the template content, and apply layouts based on rules (such as forcing a layout based on content type or metadata). Alternatively the frontend can write its own merge logic.Templates are analogous to blocks themselves but are made up of blocks with special properties. Each block in a template can be one of:
fixed: false / unset) — a named region (slotId) where editors can add their own blocks. Similar to a block field. The "default" slot receives leftover content.The slot a block lives in is identified by its slotId. This is the field name used by expandTemplates / expandTemplatesSync and by the merge rules below — not placeholder.
The point of a template is that its locked parts are authored once and update everywhere. Templates are stored as their own normal content documents; a page only references one — its blocks carry the templateId. On render the merge injects the template's fixed / fixed+readOnly blocks and fills the slots with the page's own content.
To change a template for every page that uses it:
editTemplatecontrol on the block).
fixed / fixed+readOnly blocks — in template edit mode these becomeeditable; normally they're locked.
uses that template shows the change on its next render.
What propagates and what doesn't:
pages using the template (template-controlled).
and is never written back to the template.
fixed block's content in normal mode overrides it for that page only;the template's version stays the default for other pages.
Reusing a template multiple times: a page may apply the same template more than once (e.g. two of the same layout or snippet). Each use is a distinct instance; the merge gives every instance its own block ids, so they never collide.
The two configurations below look similar but solve different problems. Pick by who controls the structure and whether it repeats:
You want… | Use | How it's applied |
|---|---|---|
A reusable snippet the editor inserts where they choose — e.g. a contact CTA reused across many pages | `allowedTemplates` | Offered in the BlockChooser's "Templates" group and inserted as a block (the block carries |
A layout forced across an entire field/region — a branded header/footer, or a mandated page structure | `allowedLayouts` | Applied across the whole blocks field; the field's content is merged into the layout's slots. The editor can't restructure it. |
To let the editor choose between a few layouts | `allowedLayouts` (several, optionally | Offered in the Layout dropdown; |
A branded header/footer is the canonical `allowedLayouts` case, not allowedTemplates: don't make the footer a templateId block the editor inserts — force a layout across the footer field. Within that layout, each block declares how locked it is:
fixed unset, with slotId) — a region where editors add their own blocks; the "default" slot receives leftover content.For a fully-fixed branded footer, leave the blocks field empty ({items: []}) and let the layout content item supply everything — then editing the layout updates every page.
Nested containers are fully supported (e.g. a branded footer built as a columns block). The merge recurses into any field whose .items array lists the block's nested block IDs — a blocks_layout-widget field of any name (a columns block's columns field counts). Two rules for nested content:
blocks map with such a sibling layout field (or the merge throws "no sibling field whose .items array lists those block IDs").slotId — the container, each column, and each block inside each column. A unique id per block (e.g. the block's own uid) works.So a branded footer is, end to end: a columns block (slotId, fixed, readOnly) → each column (slotId, fixed, readOnly) → each leaf block (slotId, fixed, readOnly).
A container lays out its children one of two ways, and the merge treats both the same — as an ordered region of child blocks:
blocks map,ordered by a blocks_layout region (as above).
of block objects, each identified by an id field (@id by default).
Everything else is identical: every object_list item still needs a slotId (plus templateId / fixed / readOnly as appropriate), and a slot item fills from the page's content just like a blocks_layout slot.
The merge identifies object_list items by their id field, and it varies per field — a form's subblocks key on field_id, a slider's slides on @id, a table's rows on key. A frontend has no schema, so whenever you expand a template or layout that contains an object_list container you MUST tell the merge each field's id field via an `idFieldMap` ({ blockType: { field: idField } }). Without it the merge falls back to @id — and for a field_id-keyed field that mints a broken id and the item is dropped on the next merge.
(On the admin this map is derived from the block schema automatically. When you re-enter to expand a single object_list array on its own, the idField shorthand is enough: expandTemplatesSync(block.slides, { templateState, templates, idField: '@id' }).)
Configure templates in page.schema.properties on the blocks field:
null allows for a no-template option. If none of those templates are already set as the layout then during editing, the first is applied automatically.Use expandTemplates (async) or expandTemplatesSync (sync with pre-fetched templates) to merge template content during rendering.
isEditMode() and pass blocks through unchanged (just adding @uid). The admin handles template merging and adds nodeId attributes for inline editing.isEditMode() returns false so templates are expanded — this is correct since edit mode only exists in the browser iframe.Sync vs Async:
`loadTemplates(data, loadTemplate)` scans page data for templateId references and loads them all in parallel. It follows nested references (templates referencing other templates) and has a 5s per-template timeout. It only loads templates actually in the page data — allowedLayouts options are loaded on demand when a forced layout is applied.
Options:
{} once per page render and share it across every expandTemplatesSync/expandTemplates call — top-level and every nested container / object_list re-entry. It records the template instances minted this render so a re-entry is recognized as already-expanded content and passed through. Never reset or recreate it mid-render (e.g. don't templateState = {} again before rendering, and don't pass a fresh {} per call): wiping it drops the minted instances, so re-entries re-apply the template instead of passing through — infinite recursion / blank page. Recognition is data-derived (by templateInstanceId), so it is safe to hand blocks back as a Vue reactive value, a clone, or a postMessage copy — no toRaw needed. (In Vue/React, provide it once at the page root via provide/inject or context; see examples/nuxt-blog-starter and examples/hydra-nextjs.)The merge algorithm follows these rules:
templateId to replace, storing any that aren't fixed and readOnly by slotId.slotId; if a slot block, don't insert it, but insert the previous blocks with the same slotId."default" if it exists, otherwise are dropped.When a layout is applied, the rules are the same but applied across a whole blocks field. Content without a slotId ends up:
"default" slot if it existsA forced layout (allowedLayouts) is applied automatically across a whole blocks field — unlike a snippet (allowedTemplates), which the editor inserts as a block where they choose (see the decision table above). Forcing is your frontend's call: you pass allowedLayouts, so you decide which layout to force and when.
Pass a static layout to always force one (e.g. a footer):
allowedLayouts is just a value you compute, so apply whatever rule you like — content type, metadata, route, A/B bucket — then pass the result. Pass undefined (or omit it) to force nothing; pass several to let the editor pick from the Layout dropdown (include null for a "no layout" option).
Note: during editing the admin side will load the templates so in order to apply the same rules of forcing a layout you will need to set allowedLayouts in page.schema.properties to ensure the page loads with the right template.