How Templates Work

A Leed site is an Eleventy 3 site rendered with Handlebars. The CMS writes your pages, your menus and your settings into your repository as files; the build turns them into HTML. Everything on this page is about the seam between those two halves: what runs in what order, which files are yours and which ones Leed drops in and deletes again, and what the single helper that every layout calls actually does to your content. The loop that runs all of it — leed site build --serve, its watcher and the local URL — is at Local Development.

Layout, template, partial and page type are four distinct objects here, and conflating them is the most common template-authoring mistake. If those words are still fuzzy, start at Core Concepts.

The render pipeline

flowchart TD
    A["Page body — Leed Markdown"] --> B["markdown-it converts it to HTML<br/>markdownTemplateEngine: false"]
    B --> C["HTML string, bound as content"]
    C --> D["Data cascade<br/>_data — 11tydata.json — front matter"]
    D --> E["eleventyComputed.layout resolves the layout"]
    E --> F["Handlebars renders the layout<br/>htmlTemplateEngine: hbs"]
    F --> G["The layout calls process content"]
    G --> H["1. Autolink — Starter and up, silent below it"]
    H --> I["2. Handlebars comments stripped"]
    I --> J["3. Embedded partial calls compiled"]
    J --> K["DOM transforms over the finished HTML<br/>pageid: rewriting — responsive image variants"]
    K --> L["Built site in .build/site/"]

Your template runs at step F. Your content arrives at it as a finished HTML string, and everything before F has already happened.

The processing order

Leed’s Eleventy configuration ends by returning three things that decide the whole order:

{
  dir: {
    data: "_data",
    includes: "_includes",
    layouts: "_layouts",
  },
  markdownTemplateEngine: false,
  htmlTemplateEngine: "hbs",
}

markdownTemplateEngine: false is the line to read twice. Markdown files are not run through a template engine at all. markdown-it converts them to HTML first, and only then does Handlebars run — over the layout, with that HTML bound to content.

Where the files come from

Three directories carry everything a template touches.

SettingValueWhat lives thereYours to edit?
dir.data_dataGlobal site data the CMS writes — menus, page types, labels, users, company settingsNo — regenerated on publish
dir.includes_includesPartialsYes, except _includes/leed/**
dir.layouts_layoutsLayoutsYes, except leed-documentation.hbs

The exceptions in that last column are there because the build writes into your repository before it runs. prepareRepo() copies the site builder’s own static/site/** tree over your src/ directory on every build and every serve. That is how _includes/leed/**, _layouts/leed-documentation.hbs, _data/eleventyComputed.js and the leed-*.hbs root templates appear in a clone that does not contain them in git.

When the build ends, cleanupRawContent() removes every one of them again: the whole _includes/leed/ directory, any leed-*.hbs, tailwind-*.css, leed-*-data.json or leed-*.png anywhere under src/, the versioned Leed JavaScript bundles, and _data/eleventyComputed.js.

There is one more thing prepareRepo does, and it is worth knowing why. Before Eleventy runs, it deletes any copy of a retired Leed template it finds — currently leed-stubs.hbs and leed-cta.hbs. Those are leftovers from an older release sitting in a repository that an older build prepared. They are gitignored, so git status cannot show you them, and Eleventy would happily build them and throw on a collection that no longer exists. Pruning them before the build is what stops a repository wedging itself on a file git will not admit exists.

The full inventory of what Leed writes into a clone, and when, is at What Leed Writes Into Your Repo; the folder-by-folder version of the “yours to edit” column is at Editable and Read-Only Files.

How a .hbs file becomes a partial

There is no registration step. Leed’s code contains no registerPartial call, and neither does your repository.

The Handlebars plugin globs _includes/**/*.hbs at startup and registers every file it finds under its path relative to the includes directory, with the extension stripped. The path is the name:

src/_includes/header.hbs         →  {{> header }}
src/_includes/cards/feature.hbs  →  {{> cards/feature }}
src/_includes/svg/logo/mark.hbs  →  {{> svg/logo/mark }}

The leed/ prefix on Leed’s own partials is not special-cased anywhere in the code. It exists for exactly one reason: Leed’s files land in _includes/leed/, so their paths — and therefore their names — start with leed/. Naming, nesting, parameters and the block form are covered in full at Writing Your Own Partials.

What process actually does

Every layout that renders page content ends up calling one helper:

{{{ process content }}}

The triple braces matter — content is HTML and double braces would escape it. Beyond that, process does three things, in this order.

StepConditionWhat happens when the condition fails
1. AutolinkThe page type has autolinking on, the site has at least one autolink phrase, the plan includes autolinking, and the page does not set disableAutolinkSkipped silently — one line in the build log, no error, nothing in the HTML
2. Strip Handlebars commentsAlways—
3. Compile embedded partial callsThe text contains a {{> … }} expression and that partial compilesThe expression is left in the page verbatim, exactly as it was typed

1. Autolinking

Autolinking rewrites configured phrases in the body into links. It runs only when the page’s page type has autolinking enabled, the site has autolink phrases defined, and the page has not opted out with disableAutolink.

2. Handlebars comments are stripped

Both forms go: the block form \{{!-- … --}} and the short form \{{! … }}. The leading backslash in each of those is the escape — it is what keeps a comment in the output, and it is the only reason this page can print the syntax at all.

This is why a comment is the right place to document a partial’s parameters: it costs the reader nothing, because it never reaches the browser.

3. Embedded partial calls are compiled

This is the step people get wrong, so it is worth being blunt about the boundary.

process scans the rendered HTML for {{> … }} expressions, compiles each one individually, and splices the result back into the string in place. A partial that fails to compile is skipped and left in the page exactly as written, which is what a stray {{> typo/name }} looks like on a live page.

That single exception is deliberate, and it is what makes the editor’s Form block work: {{> leed/form formId="…" }} typed into a page body is a partial call, so it renders. See Placing a Form on a Page for the authoring side of that.

Editing templates while the dev server runs

An edit to any .hbs file under _includes/ or _layouts/ triggers a watch target registered with resetConfig: true, so the whole Eleventy configuration re-initializes rather than just re-rendering the page.

That is heavier than it sounds, and it is deliberate, for two reasons:

  • Partials are read once. The Handlebars plugin registers every partial in its extension’s init(), and its own refresh listener never fires — it compares the glob’s ./src/_includes/x.hbs against the watcher’s normalized src/_includes/x.hbs, which never match. Without the reset, Handlebars would keep serving the partial it compiled at startup and your edit would need a restart to appear.
  • Tailwind reads your templates. The stylesheet is derived from the class names found in the @sourced templates, so a .hbs edit can introduce a utility that is not yet in the compiled CSS. The reset recompiles it.

The practical version: partial and layout edits do hot-reload, but the reload is a full config reset and is a beat slower than a content edit. If a partial edit appears to do nothing at all, check the path — you are almost certainly editing a file under _includes/leed/.

Everything you can write between the braces once you are inside a template — 64 helpers, and the rules for which ones take blocks — is cataloged at How Helpers Work. The data those templates read, and the order the cascade resolves it in, is at Global Site Data and the Data Cascade.

ESC