Files appear in your repository that you did not write, and they arrive in four distinct waves: once when the site is provisioned, whenever somebody publishes, whenever you create a page type, and on every single build. Knowing which wave a file belongs to answers the three questions you actually have about it — is it in git, may I touch it, and what do I do when it looks wrong.
Nothing here is plan-gated. Every wave below runs identically on every plan, Free included.
flowchart LR
W1["<b>Wave 1 — Provisioning</b><br/>runs once, at site creation"]
W2["<b>Wave 2 — Publishing</b><br/>runs on every publish"]
W3["<b>Wave 3 — Page type created</b><br/>runs once per page type"]
W4["<b>Wave 4 — Every build</b><br/>runs on every build and dev-server start"]
REPO[("your raw-content<br/>repository")]
W1 -->|"committed · in git · yours to edit, except the plumbing"| REPO
W2 -->|"committed · in git · read-only"| REPO
W3 -->|"committed · in git · yours to edit"| REPO
W4 -->|"gitignored · swept when the build ends"| REPO
The axis that matters is the label on each arrow. Waves 1–3 leave commits behind; wave 4 leaves nothing, which is why git status can be spotlessly clean straight after a build that obviously wrote files.
Wave 1 — what provisioning plants
When your site is created, Leed commits a starting scaffold to the repository. Every file below is a real, editable starting point except the four marked as plumbing.
| Path | What it is | Yours to edit? |
|---|---|---|
identity.json | {"companyId": "…", "environment": "…"} — how the CLI knows which site it is in | No — plumbing, restored automatically |
.gitignore | The ignore set for the whole repository | No — plumbing, restored automatically |
README.md | Repository orientation | No — refreshed by Leed, rejected at commit |
.ci/build.sh | The Cloudflare Builds entry point | No — refreshed by Leed, rejected at commit |
src/index.hbs | Your home page | Yes |
src/404.hbs | Your not-found page | Yes |
src/robots.hbs | Your robots.txt source | Yes |
src/_data/{autolink,ctaList,leedForms,menu}.json | Empty starting shapes for the generated data files | No — regenerated by the CMS |
src/_includes/{header,footer,contentCards,pagination,site-template}.hbs | The starting partial set | Yes |
src/_includes/{unsubscribe,unsubscribed}-DISABLED.hbs | Unsubscribe page sources, inert until renamed | Yes |
src/_layouts/{blog,content-list,empty}.hbs | The starting layouts | Yes |
tailwind/site.config.css | The Tailwind entry point — the one required file under tailwind/ | Yes |
tailwind/site/base.css | Base styles | Yes |
tailwind/layouts/blog.css | Blog layout styles | Yes |
Two mechanical details worth knowing. .sh files are committed with the executable bit set, so build.sh runs in CI without a chmod. And .hbs files pass through token replacement on the way in — the ###PAGE_TYPE_ID###-style placeholders described under wave 3 are substituted before the file is committed, so a freshly provisioned repository never contains one.
identity.json is written first, at repository creation, on main, with the commit message Create company identity. ctaList.json is in the scaffold but is legacy — nothing writes it any more, and it does not appear in wave 2.
Re-planting is safe
The customer-visible consequences of that are worth stating plainly, because they look like glitches otherwise. Managed files can change without any deployment appearing on your Deploy screen. And a deployment may show up carrying the reason badge Admin Rebuild, which is Leed rebuilding your site after a platform change rather than anything you did.
Wave 2 — what publishing writes
Publishing is what turns a CMS record into a file. Nothing appears in your repository before that — a draft page exists only in the CMS.
| What you published | File written | Branch | In git? |
|---|---|---|---|
| A page (to preview) | src/<pageTypeSlug>/<folder…>/<slug>.md | staging | Yes |
| A page (live) | the same path | main | Yes |
| A page with a sidecar (an API spec) | <same path>.openapi.yaml beside it | as above | Yes |
| Menus | src/_data/menu.json | main | Yes |
| Page types | src/_data/pageTypeList.json | main | Yes |
| Team members | src/_data/userList.json | main | Yes |
| Labels | src/_data/labelList.json | main | Yes |
| Forms | src/_data/leedForms.json | main | Yes |
| Autolinks | src/_data/autolink.json | main | Yes |
| Search indexes | src/_data/searchIndexList.json | main | Yes |
| Company settings | src/src.11tydata.json | main | Yes |
| Page-type settings | src/<pageTypeSlug>/<lastSegment>.11tydata.json | main | Yes |
| A logo or favicon upload | src/static/images/logo-<timestamp>.<ext> or favicon-<timestamp>.<ext> | main | Yes |
Page content
A published page becomes one markdown file: JSON front matter, then the serialized Leed Markdown body. Deleting or unpublishing the page removes the file in the same way — a delete action in the publication commit, not a leftover.
The path is not decorative. src/docs/site-repository/git-workflow.md is served at /docs/site-repository/git-workflow/, one to one, and an empty slug becomes index.md. Moving a page in the CMS moves the file here, and the file’s location is the page’s URL — the rule and everything that follows from it is a page of its own, because it is the constraint developers most often try to work around.
A page carrying a sidecar — an API page with its own OpenAPI document — gets that file written beside it with the extension swapped, and the sidecar moves with the page. A page type’s whole-spec openapi.yaml sits at the page type’s root instead, because it belongs to the set rather than to any one page.
Generated data and settings
The seven _data/*.json files, the company settings file and each page type’s 11tydata.json are all written on publish and all rewritten from scratch each time. What each one holds and how a template reads it is on the data-cascade page; the only thing to carry away here is that Leed is the author and your copy is a projection.
Brand images
A logo or favicon upload in Settings writes two commits — one deleting the previous file, one adding the new — and lands them on main rather than staging. The new file is timestamp-named so each upload is a new URL. Uploading does not trigger a build, so the change appears on the live site at the next deployment. Site identity and branding covers the upload itself, and the files are read-only afterwards.
Wave 3 — what creating a page type scaffolds
Creating a page type in the CMS commits a set of starting templates into your repository, on staging, with the subject Templates for New PageType - <slug>. Unlike everything else on this page, these files are yours — they are a starting point, and you are expected to rewrite them.
| Template | Written to | Created when | What it renders | Skipped when |
|---|---|---|---|---|
PAGE.hbs | src/<path>.hbs | You ask for a standalone template page | A single hand-authored page with front matter already filled in | — |
PAGE_TYPE_INDEX.hbs | src/<slug>-index.hbs | Always | The paginated list page for the set | — |
PAGE_LAYOUT.hbs | src/_layouts/<slug>.hbs | A layout was requested | The layout every page of the type is wrapped in | The page type’s layout is leed-documentation.hbs — that is a system file |
PAGE_TYPE_AUTHORS.hbs | src/<slug>-authors.hbs | Author pages were requested | Per-author index pages | — |
PAGE_TYPE_LABEL_INDEX.hbs | src/<slug>-<separator>-index.hbs | Label pages were requested | Per-label index pages; the separator defaults to topic | — |
PAGINATED_PAGE_TYPE_LIST.hbs | src/<slug>-list.hbs | The set is a paginated list | The list page | — |
PAGINATED_PAGE_TYPE_ITEM.hbs | src/_includes/<slug>-item.hbs | The set is a paginated list | One card in that list | — |
PAGINATED_PAGE_TYPE_PAGINATION.hbs | src/_includes/<slug>-pagination.hbs | The set is a paginated list | The pager under the list | — |
Alongside them the page type’s 11tydata.json is written from the page type record. That one is not a template and it is not yours.
Two behaviors to rely on:
Nothing is ever overwritten. Each write checks whether the file already exists on staging first, and skips it if so. Re-running the scaffold on a page type you have already customized is safe — it fills gaps and touches nothing else.
The templates are fetched at runtime from Leed’s own repository, over the GitLab API, at the moment you create the page type — not from the CLI you have installed. A page type you create today is scaffolded from today’s templates, and upgrading or not upgrading your CLI makes no difference to it. The practical consequence: a scaffolded file you saw in a colleague’s repository six months ago may not match the one you get, and neither is wrong.
Each of the eight templates is documented one by one, with the markup it produces. Creating and configuring the page type is the CMS action that triggers all of this.
The token substitutions
Every scaffolded file is a template with placeholders, substituted before the commit is made. You will never see one in a finished file — they are only visible if you go and read the source template in Leed’s own repository.
###PAGE_TYPE_ID### the new page type's id
###PAGE_TYPE_NAME### its display name
###PAGE_TYPE_SLUG### its slug
###NEW_PAGE_ID### a freshly generated UUID for a scaffolded page
###NEW_SHORT_ID### a short id, used for generated index pages
###LAYOUT_SLUG### the layout's slug
###DATE### the creation timestamp
###TITLE### the page title
###SEPARATOR### the label-index separator, default "topic"There is one more that is not substituted at scaffold time and does survive into finished files: "pageTypeId": "###ROOT###". That is a live sentinel, resolved at build time to whichever page type has an empty slug, and it is how a root-level page declares its type without hard-coding an id. It is documented with the front matter reference.
Here is what PAGE.hbs looks like before substitution, so the shape is concrete:
---
{
"title": "###TITLE###",
"publishedAt": "###DATE###",
"modifiedAt": "###DATE###",
"pageTypeId": "###PAGE_TYPE_ID###",
"pageId": "###NEW_PAGE_ID###",
"authors": [],
"keywords": []
}
---Wave 4 — what every build copies in and sweeps out
At the start of every build — and every leed site build --serve — Leed copies its own template and asset set into your working tree. At the end of the build, or when you stop the dev server, it deletes them again.
| Path or pattern | What it is | Removed when |
|---|---|---|
src/_includes/leed/** | Leed’s entire partial set — head, header, footer, docs shell, forms, menus, components | Build ends |
leed-*.hbs | The site-root templates that produce your feeds, sitemaps, _headers, _redirects and unsubscribe stubs | Build ends |
src/_data/eleventyComputed.js | The computed data layer | Build ends |
leed-*-data.json | Build-time data, such as the form partial’s country list | Build ends |
l.*.*.min.js | Leed’s versioned client scripts, written into src/static/js/ | Build ends |
tailwind-*.css | Reserved build-artifact pattern | Build ends |
leed-*.png | Reserved build-artifact pattern | Build ends |
The site-root templates and what each one produces
| Template | Output |
|---|---|
leed-sitemap.hbs | /sitemap.xml |
leed-sitemap-pageType.hbs | /sitemap-<pageTypeSlug>.xml, one per page type |
leed-feed-rss.hbs | /rss.xml |
leed-feed-atom.hbs | /atom.xml |
leed-feed-json.hbs | /feed.json |
leed-headers.hbs | /_headers |
leed-redirects.hbs | /_redirects |
leed-unsubscribe.hbs | /stubs/unsubscribe.html |
leed-unsubscribed.hbs | /stubs/unsubscribed.html |
None of these exists in your repository between builds, which is why editing your _headers is not a thing you can do. What the feeds, sitemaps and robots.txt contain covers the output; this list is only about where it comes from.
Leed’s partials under src/_includes/leed/ are replaced wholesale on every build, so editing one is pointless — the edit is gone before the build finishes. The full partial index, and which of them you may take over explains the supported way to override one.
Retired templates are pruned before the build, not after
Two templates Leed used to ship — leed-stubs.hbs and leed-cta.hbs — are deleted at the start of every build rather than at the end. That looks redundant and is not.
A repository prepared by an older release still has Leed’s copy sitting in it. It is gitignored, so git status never mentions it, but Eleventy still finds it and still builds it — and one of the two paginates over a collection that no longer exists, so the build throws. Because the throw happens before the build reaches its cleanup step, the stale file is never removed, and the next build fails at exactly the same place. That is self-wedging: without hand-deleting a file git does not show you, the repository cannot recover.
Pruning before Eleventy runs is what breaks that loop. If you ever see a build fail on a file you cannot find, this is the shape of the problem, and running a current build is the fix.
What to do when a generated file looks wrong
Work through it in this order; the first question that gets a yes is your answer.
Is it stale? Pull. The CMS may have published while you were working, and your copy is simply older than the remote’s. If it is still wrong after a pull, republish the settings screen that owns it — the file is regenerated from CMS state, so republishing rewrites it.
Is it ephemeral? If the file matches one of the wave-4 patterns and is missing, that is correct: it only exists during a build. Run leed site build and it will be there.
Is it hand-edited? If you or somebody else changed a CMS-owned file locally, leed site validate --reset puts it back — after you have run leed site validate --reset --dry-run to see what it is about to touch. Which paths are CMS-owned, and what reset does to each kind of change is the page to read first.
Every write on this page arrives as a commit, and the commit each one produces, with its author and branch is cataloged alongside your own. If you want the step before that — what publishing actually is, and what it does besides writing files — start with how publishing works.