Model Templates
Model templates are EJS files that turn content into HTML. Every page model, entry model, and block model can be bound to a template, and the binding is implicit — the template's filename matches the model's key. When the builder renders a record, it runs the matching template with the record's content as item.
This page covers the three template types, the naming convention that links templates to models, what each template receives, and a few rules to keep in mind when writing them.
Three Template Types
The site builder distinguishes three template categories, each living under a different folder:
| Folder | Used for | When it renders |
|---|---|---|
pages/ |
Page model templates | Once per page record — produces an HTML file at the record's path |
blocks/ |
Block model templates | Each time a block instance is rendered via render() |
entries/ |
Entry model templates | Each time an entry is rendered via render() (optional — entries are usually consumed as data) |
A page template is the entry point for an HTML file. A block template is invoked by a parent template that calls render() on a block value. An entry template is the same idea but for entries — useful when an entry needs a consistent rendered fragment across many parent contexts (a team-member card, an author bio).
Naming Convention
The link between a model and its template is the key — the filename without the extension. A page model at models/pages/about.model has its template at pages/about.ejs. A block model at models/blocks/hero.model has its template at blocks/hero.ejs.
For collections, the + suffix is part of the key and appears on every file:
| Model file | Template file |
|---|---|
models/pages/about.model |
pages/about.ejs |
models/pages/blog+.model |
pages/blog+.ejs |
models/entries/testimonials+.model |
entries/testimonials+.ejs |
models/blocks/hero.model |
blocks/hero.ejs |
The CMS resolves the pairing automatically. There's no template registration step — adding a .ejs file with the right name and a matching .model is the entire binding.
For pages whose key contains _, the underscore maps to / in the URL (e.g., docs_getting-started.ejs → /docs/getting-started). The key still matches the model file one-for-one.
What Each Template Receives
Every template is called with the current record as item, plus the full site content as named variables and helper functions:
item — The current record's content. In a page template, item is the page record (with _path, _slug, and _meta). In a block template, item is the block instance. In an entry template, item is the entry.
Site-wide context — pages, entries, images, and options are available everywhere. Most templates use the helper functions (getPage, getPages, getEntry, getImage, getOptions) instead of touching these directly.
Helpers — render() for delegating to other templates, src()/img()/picture()/svg() for images, embed() for videos, path() and url() for links, and head-injection helpers (title, meta, link, script, style).
The full reference is on Template Context and Data Access.
A Page Template
A page template renders a single page's HTML. The simplest possible page template:
<% title(item.title) %>
<h1><%= item.title %></h1>
<%- marked(item.content) %>
Most page templates do a bit more — they include SEO metadata, render block stacks, pull in shared entries, and link assets:
<% title(item.title + ' | My Site') %>
<% meta({ name: 'description', content: item.summary }) %>
<% link({ rel: 'canonical', href: url(item) }) %>
<article>
<h1><%= item.title %></h1>
<time datetime="<%= item.published_at %>"><%= item.published_at %></time>
<% if (item.author) { %>
<p>By <%= item.author.name %></p>
<% } %>
<%- img(item.hero, '1200x600') %>
<%- render(item.body) %>
</article>
The page template doesn't define the outer HTML shell — <html>, <head>, navigation, footer — that's the layout's job (Layout).
A Block Template
A block template renders one block instance. It receives the block's field data as item and outputs the HTML for that block:
<section class="hero" style="background-image: url('<%- src(item.background, '1920x800') %>')">
<h1><%= item.heading %></h1>
<p><%= item.subheading %></p>
<% if (item.cta_link) { %>
<a href="<%= item.cta_link %>"><%= item.cta_label %></a>
<% } %>
</section>
When a page contains a block field or a stack, the page template calls render() to dispatch to the block template:
<%- render(item.hero) %> <!-- single block(hero) field -->
<%- render(item.sections) %> <!-- stack field — dispatches per _block -->
Each rendered block gets its own item scope, so a block template can be developed in isolation from the pages that use it.
→ Block Fields → Stack
An Entry Template
Entry templates are optional. Most entries are consumed as data (item.author.name, getEntry('footer').copyright_text) — you don't need a template for an entry just to read its fields. You add an entry template when the entry has a recurring visual representation that you want to centralize:
<!-- entries/people+.ejs -->
<article class="person">
<%- picture(item.photo, '128x128') %>
<h3><%= item.name %></h3>
<p class="role"><%= item.role %></p>
<p><%= item.bio %></p>
</article>
Then any template can render an entry through it:
<% for (const person of getEntry('people+')) { %>
<%- render(person) %>
<% } %>
If an entry doesn't have a template, calling render() on it returns an empty string — no error.
Rules to Keep in Mind
A few patterns trip up most first templates:
Escape user content, output helper HTML unescaped. Use <%= %> for text fields (title, name) and <%- %> for helpers that return HTML (render, img, picture, marked).
markdown fields are raw markdown, not HTML. Wrap with marked(): <%- marked(item.content) %>. richtext fields are already HTML — use <%- directly.
Page templates don't define <html> or <head>. That's the layout's job. A page template renders the page body; the layout wraps it. See Layout.
Don't inline <link> or <script> tags. Use the head-injection helpers link() and script() — they deduplicate and respect ordering.
Templates can call any helper from any depth. A block template can call getEntry(), set page-level metadata via title()/meta(), and pull in shared options. The full template context is available everywhere.
→ Template Context and Data Access
What's Next
- Layout — The wrapper that frames every page.
- Template Context and Data Access — The complete reference for
item, helpers, and content access. - Block Models — Defining blocks that templates render.
- Stack — Composable block sections and dispatch via
render(). - Site Builder Architecture — The build pipeline from content to HTML.