Overriding Leed Templates

Leed ships the markup for its documentation chrome, its unsubscribe pages and its recommendation cards. Six named files in src/_includes/ let you replace part of it. This is the only supported way to change a Leed template, because everything under _includes/leed/ is copied into your repository at the start of a build and deleted at the end of it — an edit there does not survive the run that made it.

The six slots

Create this fileReplacesDetected byRequiresEjectable?
docs-header.hbsthe documentation header, inside Leed’s positioned wrapperuseCustomTemplate "docsHeader" + hasTier "starter"Starteryes
docs-footer.hbsthe documentation footer, inside #docs-footer-wrapperuseCustomTemplate "docsFooter" + hasTier "starter"Starteryes
unsubscribe.hbsthe /stubs/unsubscribe.html pageuseCustomTemplate "unsubscribe"free on every planyes
unsubscribed.hbsthe post-unsubscribe confirmation pageuseCustomTemplate "unsubscribed"free on every planyes
stub-template.hbsthe recommendation card cloned in the browsera direct file lookup by resolveStubTemplateGrowth, via the recommendationEngine featureyes
header-includes.hbsnothing — it is added to every page’s <head>templateExists "header-includes.hbs"free on every planno Leed default, nothing to eject

The structural fact underneath the whole table: an override is detected by a file existing. There is no toggle in the CMS, no setting to flip and no field to fill in. Create the file, and the next build finds it.

How detection works

Three mechanisms, and the differences are not academic — one is a snapshot taken once, one is checked live, and one bypasses both.

customTemplates

Four of the slots are read through a single object built once, at build start, by probing src/_includes/ on disk:

docsFooter    ← src/_includes/docs-footer.hbs
docsHeader    ← src/_includes/docs-header.hbs
unsubscribe   ← src/_includes/unsubscribe.hbs
unsubscribed  ← src/_includes/unsubscribed.hbs

A template then asks {{ useCustomTemplate "docsHeader" }}. Because the probe runs at build start, creating one of these four files while leed site build --serve is already running does not pick it up on the next reload — restart the build so the probe runs again.

templateExists and fileLookup

templateExists "<filename>" is a live fs.existsSync under the includes directory, evaluated at render time rather than cached. Its one production call site is line 1 of leed/head.hbs, which is what makes header-includes.hbs work.

stub-template.hbs uses a third path entirely. resolveStubTemplate prefers _includes/stub-template.hbs over _includes/leed/stub-template.hbs, reads whichever it finds and compiles it once per build into a <template id="leed-stub-template"> element that the browser clones for each recommendation. Because the cloning happens client-side, the file is not a Handlebars template in the usual sense: it uses data-leed-slot and data-leed-each attributes, not {{ }} expressions.

<a class="page-stub" data-leed-slot="url">
    <div class="title" data-leed-slot="title"></div>
    <div class="summary" data-leed-slot="summary"></div>
    <template data-leed-each="labels"><li data-leed-slot="name"></li></template>
</a>

The whole path, end to end

Three detection mechanisms, two kinds of gate and a fail-closed default make “I created the file, why is nothing different?” a question with several possible answers. This is the branch to walk:

flowchart TD
  E["leed site eject &lt;name&gt;"] --> F["src/_includes/&lt;name&gt;.hbs exists"]
  F --> M{"Which mechanism?"}
  M -->|"docs-header, docs-footer,<br/>unsubscribe, unsubscribed"| P["customTemplates probe<br/>runs once at build start"]
  M -->|"header-includes"| T["templateExists<br/>live, at render time"]
  M -->|"stub-template"| R["resolveStubTemplate<br/>file lookup, once per build"]
  P --> U{"useCustomTemplate true?"}
  U -->|no| D["Leed's default renders"]
  U -->|yes| G{"hasTier / hasFeature<br/>satisfied?"}
  G -->|"no — including<br/>an unreadable tier"| D
  G -->|yes| Y["Your file renders"]
  T --> Y
  R --> Y
  D --> BADGE["Footer only: the same gate's else branch<br/>renders 'Powered by Leed'"]

leed site eject

Start here rather than by hand-writing a file. With no argument the command lists what your plan lets you customize:

  name            status         destination                      description
  stub-template   Leed default   src/_includes/stub-template.hbs  Recommendation card markup, cloned once per recommendation in the browser.
  unsubscribe     Leed default   src/_includes/unsubscribe.hbs    Email unsubscribe page — full page replacement.
  unsubscribed    Leed default   src/_includes/unsubscribed.hbs   Post-unsubscribe confirmation page — full page replacement.
  docs-header     Leed default   src/_includes/docs-header.hbs    Documentation header, inside Leed's positioned wrapper. A working starting example.
  docs-footer     customized     src/_includes/docs-footer.hbs    Documentation footer. The free tier always keeps Leed's, so the licensing badge stays.

	leed site eject <name>

status is customized when a file already exists at the destination and Leed default when it does not. With a name, the command copies the Leed source to that destination, creating directories as needed, and prints Wrote src/_includes/docs-header.hbs — edit and commit it to take ownership. Overwriting an existing customization needs -f, --force; without it the command fails rather than destroying your work.

leed site eject                    # what can this site customize?
leed site eject docs-header        # write it
leed site eject docs-header -f     # overwrite an existing customization
Every failure the command can report
SituationMessage
Name is not in the registryUnknown template '<name>'. Valid names: stub-template, unsubscribe, unsubscribed, docs-header, docs-footer — with the hint Run leed site eject with no template name to list the templates this site can eject
Name is real but gated above your plan'<name>' requires the <tier> plan or above; this site is on <tier>. — and this site is on an unknown plan when no tier could be read at all
Destination already exists, no --forcesrc/_includes/<name>.hbs already exists. Pass --force to overwrite your customization.
The installed CLI is missing its own source fileThe Leed default for '<name>' is missing at <path> — the hint is to run leed self-update
Your plan gates every templateNo templates are available to customize on your current plan.

Tier behavior

A template above your plan is not listed at all. Naming it explicitly fails with the message above rather than writing a file the renderer would ignore — handing you a file that cannot work is worse than not offering it.

Tier comes from src/src.11tydata.json through the same resolution a build uses: in CI only LEED_ENTITLEMENTS is trusted, and locally the repository file is the fallback.

Why the source is always a real partial

The registry’s design rule is that an ejectable template’s source is the file Leed itself renders, never a hand-written scaffold. A parallel scaffold diverges from the shipped markup the first time either side is edited, and hands you a starting point that is already wrong. The two unsubscribe pages are the one exception, and a pre-existing one: their defaults live inline in the {{else}} branch of Leed’s unsubscribe page, so there is no partial to point at — those eject from a scaffold that has shipped in every repository for years.

That rule has a consequence worth setting expectations for before you run the command.

The gates, and the badge you cannot remove

Two shapes of requirement exist. A { tier } requirement mirrors a literal hasTier "starter" guard inside the template that renders the slot; because the value is written in two places, a drift test fails the build if the registry and the template ever disagree. A { feature } requirement resolves through the shared feature catalog instead, so if a feature’s minimum tier moves, the eject gate follows with no edit anywhere. stub-template uses the feature form, keyed on recommendationEngine — which is Growth today.

The gate on stub-template behaves differently again, and it is worth knowing which side fails: below Growth the recommendations endpoint returns nothing at all, so the template renders no cards however carefully it is written. That is a data gate, not a markup gate — see Recommendations.

header-includes.hbs is not in the registry

It has no Leed default, so there is nothing to eject: it activates purely by existing, and its whole job is to add to every page’s <head> rather than replace anything. It is also the only hook that reaches documentation pages and marketing pages alike. Everything it can carry is at Customizing the <head>.

What each slot is replacing

Two of the six sit inside structures worth understanding before you edit them. The documentation header and footer live inside a measured, positioned shell whose ids and Alpine state your markup has to cooperate with — Documentation Shell Partials maps what is above and below each slot, and the branch inside leed/head that reads header-includes.hbs is described at Head and Component Partials. Replacing the unsubscribe pages changes what a recipient sees at the end of the opt-out flow, covered at Unsubscribes and Opt-Outs.

Committing your override

An ejected file is an ordinary repository file from the moment it is written. Validate it, commit it and push it the same way as any other change — the four-step contract is at Validate, Commit and Push. Handlebars files under _includes/ are exempt from the blocked-extension list but are still subject to the per-file size limit, which no template should come close to.

The command’s own flags, exit codes and machine-readable output belong to the CLI reference at leed site eject; this page is about what each template is.

ESC