What Leed Writes Into Your Repo

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.

PathWhat it isYours to edit?
identity.json{"companyId": "…", "environment": "…"} — how the CLI knows which site it is inNo — plumbing, restored automatically
.gitignoreThe ignore set for the whole repositoryNo — plumbing, restored automatically
README.mdRepository orientationNo — refreshed by Leed, rejected at commit
.ci/build.shThe Cloudflare Builds entry pointNo — refreshed by Leed, rejected at commit
src/index.hbsYour home pageYes
src/404.hbsYour not-found pageYes
src/robots.hbsYour robots.txt sourceYes
src/_data/{autolink,ctaList,leedForms,menu}.jsonEmpty starting shapes for the generated data filesNo — regenerated by the CMS
src/_includes/{header,footer,contentCards,pagination,site-template}.hbsThe starting partial setYes
src/_includes/{unsubscribe,unsubscribed}-DISABLED.hbsUnsubscribe page sources, inert until renamedYes
src/_layouts/{blog,content-list,empty}.hbsThe starting layoutsYes
tailwind/site.config.cssThe Tailwind entry point — the one required file under tailwind/Yes
tailwind/site/base.cssBase stylesYes
tailwind/layouts/blog.cssBlog layout stylesYes

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 publishedFile writtenBranchIn git?
A page (to preview)src/<pageTypeSlug>/<folder…>/<slug>.mdstagingYes
A page (live)the same pathmainYes
A page with a sidecar (an API spec)<same path>.openapi.yaml beside itas aboveYes
Menussrc/_data/menu.jsonmainYes
Page typessrc/_data/pageTypeList.jsonmainYes
Team memberssrc/_data/userList.jsonmainYes
Labelssrc/_data/labelList.jsonmainYes
Formssrc/_data/leedForms.jsonmainYes
Autolinkssrc/_data/autolink.jsonmainYes
Search indexessrc/_data/searchIndexList.jsonmainYes
Company settingssrc/src.11tydata.jsonmainYes
Page-type settingssrc/<pageTypeSlug>/<lastSegment>.11tydata.jsonmainYes
A logo or favicon uploadsrc/static/images/logo-<timestamp>.<ext> or favicon-<timestamp>.<ext>mainYes

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.

TemplateWritten toCreated whenWhat it rendersSkipped when
PAGE.hbssrc/<path>.hbsYou ask for a standalone template pageA single hand-authored page with front matter already filled in—
PAGE_TYPE_INDEX.hbssrc/<slug>-index.hbsAlwaysThe paginated list page for the set—
PAGE_LAYOUT.hbssrc/_layouts/<slug>.hbsA layout was requestedThe layout every page of the type is wrapped inThe page type’s layout is leed-documentation.hbs — that is a system file
PAGE_TYPE_AUTHORS.hbssrc/<slug>-authors.hbsAuthor pages were requestedPer-author index pages—
PAGE_TYPE_LABEL_INDEX.hbssrc/<slug>-<separator>-index.hbsLabel pages were requestedPer-label index pages; the separator defaults to topic—
PAGINATED_PAGE_TYPE_LIST.hbssrc/<slug>-list.hbsThe set is a paginated listThe list page—
PAGINATED_PAGE_TYPE_ITEM.hbssrc/_includes/<slug>-item.hbsThe set is a paginated listOne card in that list—
PAGINATED_PAGE_TYPE_PAGINATION.hbssrc/_includes/<slug>-pagination.hbsThe set is a paginated listThe 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 patternWhat it isRemoved when
src/_includes/leed/**Leed’s entire partial set — head, header, footer, docs shell, forms, menus, componentsBuild ends
leed-*.hbsThe site-root templates that produce your feeds, sitemaps, _headers, _redirects and unsubscribe stubsBuild ends
src/_data/eleventyComputed.jsThe computed data layerBuild ends
leed-*-data.jsonBuild-time data, such as the form partial’s country listBuild ends
l.*.*.min.jsLeed’s versioned client scripts, written into src/static/js/Build ends
tailwind-*.cssReserved build-artifact patternBuild ends
leed-*.pngReserved build-artifact patternBuild ends
The site-root templates and what each one produces
TemplateOutput
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.

ESC