Templates (hbs/)
Handlebars, rendered in a single pass: a page wraps itself in its layout partial. Nothing about the frontend is baked into the binary — every page of this site, admin included, is an hbs file you can edit (or point an agent at).
Layout and slots
The skeleton is an ordinary entry in the root partials map (this site declares _layout for public pages and _cms for the admin). A page opens with {{#> _layout}}, defines its content as an inline slot — {{#*inline "content"}} … {{/inline}} — and closes with {{/_layout}}. The skeleton prints the slot where it wants it: {{#> content}}fallback{{/content}} — the text between is used when a page does not define the slot, and a skeleton can consume several named slots.
Partials
Templates declared in partials are included with {{> name}}. The declaration is explicit on purpose: an undeclared {{> x}}, or a declared partial without a file, fails cms check and startup instead of erroring on the first request. A partial may declare its own data ("views": { "key": "view_name" }): every public page that includes it gets those blocks in the context automatically — list views only, no URL filters, and views bound in cms.menu are refused here, so admin data cannot leak through a public partial.
Page context
| context variable | available in |
|---|---|
| rows, total, per_page, next_cursor | pages with a list view (limit set) |
| row | pages with a single-row view |
| params | always: path and query parameters |
Loop example: {{#each rows}} … {{title}} … {{/each}}, with {{else}} for the empty state. Raw HTML from data (like this article body) is printed with triple braces.
Helpers and conventions
One custom helper: split — a comma-separated text column becomes a real array at render time. Several photos per record without a new column type: {{lookup (split row.photos) 0}} for the first, {{#each (split row.photos)}}…{{/each}} for all (trimmed, empty pieces dropped). Every template reference in the config names the file with its .hbs extension.