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).

→ Site Builder Architecture


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