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 contextpages, entries, images, and options are available everywhere. Most templates use the helper functions (getPage, getPages, getEntry, getImage, getOptions) instead of touching these directly.

Helpersrender() 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 FieldsStack


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