require(), include(), cms.partial() — and the editor and API lanes.
Every way one template gets into another render — from hand-written EJS to the editor to the API. The map first, then each lane:
| Lane | Where you write it | Resolves by | Best for |
|---|---|---|---|
require() |
Any site/theme EJS | File path, over the fork chain | Fixed chrome and composition in code |
include() |
Any EJS | File path, same physical tree | Quick same-folder includes |
cms.partial() |
Document templates | Entity (type-first, then partial id) | Deprecated — use layout bands (docs/CHROME_COMPOSITION.md) |
| Editor embed | The page editor (/) |
Content type | Author-placed bands |
| Section insert | The section chooser | Content type (fragment with regions) | Editable structure in the body |
| Layout composition | The Layouts admin | Visual placement | Composed body arrangements |
| Composition API | HTTP | Zone id | Machines: agents, importers |
require()The house mechanism for file-to-file composition. It returns a curried render function — one call resolves, the second renders:
<%- require('/site/templates/chrome/navbar.ejs')() %>
<%- require('/site/templates/chrome/navbar.ejs')({ active: 'home' }) %>
<%- require('./chrome/navbar.ejs')() %>
Path grammar — a ref must start with one of:
| Prefix | Meaning |
|---|---|
/site/<rel>.ejs |
Absolute against the site's root chain: the site's own files first, the theme package second. A require from a theme file still picks up the site's fork of that child. |
./ and ../ |
Relative to the requiring file, resolved over the same chain. |
/accent/… |
App-wide shared partials. |
Rules worth knowing:
.ejs extension is required. A ref without it is treated as a
JS module require — require('/site/utils/format.js') is also a thing,
for shared helpers.require('chrome/navbar.ejs') is not in the grammar).locals — the child reads locals.active.
System context (cms, require, theme locals) is inherited
automatically and stays out of the props bag.include()Stock EJS, resolved relative to the including file's location on disk:
<%- include('chrome/navbar') %>
<%- include('../partials/card', { title: 'Hi' }) %>
It works, with two limits require() doesn't have: both files must live in
the same physical tree (a theme file cannot include a site fork, and
the fork chain is not consulted), and the child shares the including
template's scope rather than getting a clean locals. Fine for a quick
same-folder split; prefer require() for anything structural.
cms.partial()DEPRECATED (2026-08-13,
docs/CHROME_COMPOSITION.md). Chrome placement is per-site composition data — a layout band with explicittypeId+assetId— not a template call. New introductions are rejected at the write boundary (400 cms_partial_deprecated) and flagged by the boot guard; existing call sites keep rendering (a failed resolution now degrades to an HTML comment instead of killing the page) until phase 1 migrates them to bands. The description below documents the frozen behavior. Note the name collision: inside a partial's own render,cms.partialis that partial's context object — that is a different thing and is not deprecated.
The entity lane, available inside document templates: mount by name, not by file path.
<%- cms.partial('navbar')() %>
<%- cms.partial('chrome/footer')({ compact: true }) %>
Curried like require. Resolution is type-first:
Merge order is placement-wins: instance data is the base, caller props override.
Why choose it over require(): entity mounts ride the uid, so slug
renames are free; and the type-first branch carries data. Limits: it
exists only in document-template render lanes (inside a partial's own
render, cms.partial is that partial's context object, not a function),
and file-path refs don't belong here — that's require()'s job.
Covered in their own chapters; listed here so the map is complete:
/ in the page editor places a type-addressed band; it
re-renders through the type's current renderer on every render. See
Rendering content types.The composition verb embeds a partial or type instance into a named
zone by id (primary, or any authored data-zone) — no positions, no
labels. See The web API.
Chrome fixed by the designer → require(). Chrome managed in the CMS,
possibly with content → a layout band (make it a type, place it in the
type's Layout — cms.partial() is deprecated, docs/CHROME_COMPOSITION.md).
Placed by authors per page → embed or section. Arranged visually →
Layout. Driven by software → the API.