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 file | Replaces | Detected by | Requires | Ejectable? |
|---|---|---|---|---|
docs-header.hbs | the documentation header, inside Leed’s positioned wrapper | useCustomTemplate "docsHeader" + hasTier "starter" | Starter | yes |
docs-footer.hbs | the documentation footer, inside #docs-footer-wrapper | useCustomTemplate "docsFooter" + hasTier "starter" | Starter | yes |
unsubscribe.hbs | the /stubs/unsubscribe.html page | useCustomTemplate "unsubscribe" | free on every plan | yes |
unsubscribed.hbs | the post-unsubscribe confirmation page | useCustomTemplate "unsubscribed" | free on every plan | yes |
stub-template.hbs | the recommendation card cloned in the browser | a direct file lookup by resolveStubTemplate | Growth, via the recommendationEngine feature | yes |
header-includes.hbs | nothing — it is added to every page’s <head> | templateExists "header-includes.hbs" | free on every plan | no 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.hbsA 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 <name>"] --> F["src/_includes/<name>.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 customizationEvery failure the command can report
| Situation | Message |
|---|---|
| Name is not in the registry | Unknown 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 --force | src/_includes/<name>.hbs already exists. Pass --force to overwrite your customization. |
| The installed CLI is missing its own source file | The Leed default for '<name>' is missing at <path> — the hint is to run leed self-update |
| Your plan gates every template | No 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.