When you create a page type in the CMS, Leed commits starter templates to your content repository so the set renders on the very next build instead of 404ing until you write a template by hand. Those files are starting points you own. They are committed once, they are never touched again, and the moment you edit one it is yours — nothing in the product will come back and reconcile it.
What a new page type actually writes
Creating a page type through the CMS commits up to three files — and at most two for a documentation or API set. All of them land in one commit at the end of the create. A workspace that has no content repository yet still gets its page type; the scaffolding is simply skipped. When there is a repository and the scaffold commit fails, the page type (and any menu created with it) is rolled back, so the retry is not refused for a slug that is already taken.
| File | Written when | Purpose |
|---|---|---|
src/<slug>-index.hbs | The page type is created without a layout of its own, unless the site already has src/<slug>/index.* or src/<slug>.* | The paginated card grid over the page type’s :paged collection |
src/<slug>/<lastSlugSegment>.11tydata.json | Always (any page type with a slug) | Eleventy directory data holding the page type’s site-builder settings |
src/_layouts/empty.hbs | The page type is created with no layout, or with empty.hbs, and the file is not there yet | The default layout: {{{ process content }}} and nothing else |
Which of those you get depends on the layout the create names. With none, the page type is bound to empty.hbs, which the CMS writes itself if your repository lacks it, and the list page is scaffolded beside it. empty.hbs is the page content and nothing else — no site-template wrapper — so a page of that type publishes as bare content until you bind the type to a layout of your own. With a layout of your own, that file must already exist in src/_layouts/ on staging: if it does not, the create is refused with a 400 naming the layout field (Layout "…" does not exist in src/_layouts/ on the site repo.), and if it does, the CMS writes neither a layout nor a list page — whatever supplied the layout supplies its list page too. The same existence check runs when a page type’s layout is changed. The index is skipped when your site already renders /<slug>/index.html itself — a section index template, or a top-level src/<slug>.hbs, .md, .njk or any other Eleventy template format, which Eleventy’s directory-style permalink renders to the same address. Writing it anyway would give two input files one output path and fail the build.
That is the whole list. The authors index and the label index are no longer templates the CMS holds: they moved, with the blog layout, its card and pagination partials and its list page, into the blog recipe that /leed:create blog applies from the Leed plugin for Claude. A new company is provisioned with only a Root page type and a getting-started page, and no starter blog.
If you want an authors index or a label index without the recipe, the recipe’s blog-authors.hbs and blog-topic-index.hbs are the shapes to copy: each paginates the :authors or :labels collection described at Collections and Pagination Data.
Creating a page type named guides with no layout commits these paths to staging, in one commit titled Templates for New PageType - guides:
src/guides-index.hbs
src/_layouts/empty.hbs only when the site did not have one
src/guides/guides.11tydata.jsonThe templates
| Template | Written to | Written by | When | Purpose |
|---|---|---|---|---|
PAGE.hbs | src/<path>.hbs | createTemplatePage | Workspace seeding only, as the home page when the provisioned scaffold has no src/index.hbs | A single standalone page |
PAGE_TYPE_INDEX.hbs | src/<slug>-index.hbs | createNewPageType | On page-type create without a layout of its own, skipped when the site already has its own /<slug>/ index | Paginated card grid over :paged |
PAGE_LAYOUT.hbs | src/_layouts/<layout> | createNewPageType | Not reached by the CMS create path, which writes empty.hbs instead — copy it by hand | A layout wrapping your master template |
PAGINATED_PAGE_TYPE_LIST.hbs | src/<slug>-list.hbs | createPaginatedPageTypeList | Not reachable from the CMS — copy by hand | The driver for a noRender page type |
PAGINATED_PAGE_TYPE_ITEM.hbs | src/_includes/<slug>-item.hbs | createPaginatedPageTypeList | Not reachable from the CMS — copy by hand | The article body for that set |
PAGINATED_PAGE_TYPE_PAGINATION.hbs | src/_includes/<slug>-pagination.hbs | createPaginatedPageTypeList | Not reachable from the CMS — copy by hand | First / prev / next / last controls |
(not a template) empty.hbs | src/_layouts/empty.hbs | createNewPageType | On create with no layout or empty.hbs, when the file is missing | {{{ process content }}} and nothing else |
(not a template) .11tydata.json | src/<slug>/<lastSlugSegment>.11tydata.json | create11tyData | Always, on page-type create | Generated from the page type’s own settings, not from a template file |
The last row matters when you go looking for it: the directory data file has no template. It is JSON.stringify of the page type’s site-builder data, so its contents change with the page type’s settings rather than with a file in Leed’s template folder. How that file joins the data cascade is covered in Global Site Data.
PAGE_TYPE_INDEX.hbs
The index paginates collections.pageTypeId-<id>:paged, which is already chunked, so size is 1 — one chunk per output page. Two lines in its front matter are worth reading before you edit it:
{
"title": "Blog List",
"publishedAt": "list",
"modifiedAt": "list",
"pageTypeId": "100111",
"pagination": {
"data": "collections.pageTypeId-100111:paged",
"size": 1,
"alias": "list",
"addAllPagesToCollections": true,
"generatePageOnEmptyData": false,
"reverse": false
},
"pageId": "3f1a9c04",
"permalink": "{{ pageType this.pageTypeId 'slug' }}{{#unless list.page.last }}/page/{{ add list.page.number 1 }}{{/unless}}/index.html"
}generatePageOnEmptyData: false means a page type with no published pages produces no index at all, rather than an empty grid.
The computed permalink is the line to read twice. :paged is chunked from a collection sorted ascending by publishedAt, so chunk 0 holds the oldest pages and page.last is true on the chunk holding the newest. {{#unless list.page.last }} therefore drops the page segment from the newest chunk, which is what makes /blog/ the set’s landing page; the older chunks become /blog/page/1/, /blog/page/2/ and so on, numbered from the oldest. Change that expression and you change every URL in the set, so treat it as a URL decision rather than a formatting one.
The body wraps everything in {{#>site-template}} and iterates {{#each (reverse list.pageData)}} — the reverse is what puts the newest card first within a chunk. The collections and the PageData object it walks are documented at Collections and Pagination Data.
PAGE_LAYOUT.hbs
No create path writes this one any more — a new page type gets empty.hbs instead — but it is still the shape to start from when you write a layout that wraps your master template. It is deliberately small: a wrapper, an <h1>, and the content:
{{#>site-template}}
<main class="relative mt-8 mx-6 md:mx-8">
<section id="template-content">
<article id="guides-copy">
<h1>{{{ title }}}</h1>
<!-- START guides CONTENT-->
{{{ process content }}}
<!-- END guides CONTENT-->
</article>
</section>
</main>
{{/site-template}}The shipped file also wraps that body in a pair of START / END Handlebars block comments, omitted here.
The paginated trio
PAGINATED_PAGE_TYPE_LIST.hbs, plus <slug>-item.hbs and <slug>-pagination.hbs in _includes, are the pattern for a page type whose pages your template generates rather than the site builder. Set the page type to Do Not Render (noRender), and the list template takes over every URL in the set:
{
"pagination": {
"data": "collections.pageTypeId-100111:unpaged",
"size": 1,
"alias": "item",
"addAllPagesToCollections": true,
"reverse": false,
"resolve": "values"
},
"forceRender": true,
"permalink": "{{ pageType this.pageTypeId 'slug' }}/{{ item.data.slug }}/index.html"
}Three things make it work. forceRender: true overrides the noRender suppression for this one file. The :unpaged collection is a flat list rather than chunks, so size: 1 yields one output page per content page. And the permalink is built from item.data.slug, which is why the template — not Leed — owns the path of every page in the set. noRender and its consequences are covered at Layouts and Page Types.
Reach for this when a set’s URLs cannot be expressed by the standard page-type slug plus page slug: an imported archive that has to keep its legacy paths, or a generated set numbered rather than named.
The full PAGINATED_PAGE_TYPE_LIST.hbs body
Everything after the front matter. Note that it wraps the whole document in {{#with item.data}} so the page renders in the context of the item, not the driver page, and that the two _includes partials are named after the page type’s slug:
{{#with item.data }}
{{#>site-template}}
<main class="relative mt-8 mx-6 md:mx-8">
{{> guides-pagination }}
{{> guides-item }}
{{> guides-pagination }}
</main>
{{/site-template}}
{{/with}}guides-item.hbs renders the article, and because it runs inside the {{#with}} scope it reaches back out for the body with {{{ process @root/item.content }}} — the item’s content is not in scope otherwise. guides-pagination.hbs opens with {{#with @root }}, for the same reason: pagination.href.first / .previous / .next / .last live at the root, not on the item.
The tokens
The shipped templates carry ###TOKEN### placeholders. Substitution is literal string replacement performed once, at commit time, so the tokens never appear in your repository — this table is for reading the shipped templates, not for authoring.
| Token | Resolves to |
|---|---|
###PAGE_TYPE_ID### | The page type’s id (for example 100111) |
###PAGE_TYPE_NAME### | The page type’s display name |
###PAGE_TYPE_SLUG### | The page type’s slug |
###NEW_PAGE_ID### | A freshly minted UUID v4 for the generated page |
###NEW_SHORT_ID### | The first 8 characters of that same UUID |
###LAYOUT_SLUG### | The page type’s layout filename without .hbs — used only in PAGE_LAYOUT.hbs |
###DATE### | The commit moment, as an ISO-8601 timestamp |
###TITLE### | The page title — used only in PAGE.hbs |
###SEPARATOR### | The label URL segment, default topic |
###SEPARATOR### is the label URL segment — the blog recipe’s topic index uses it to put label pages at /blog/topic/<label>/. The CMS create path never passes a separator, so it is always topic there.
One placeholder-looking string is not a substitution: ###ROOT###. It is a live sentinel value that stays in the file. A page whose front matter carries "pageTypeId": "###ROOT###" is resolved at build time to whichever page type has the empty slug, which is how Leed’s own unsubscribe pages attach themselves to your site’s root page type without knowing its id.
Where the templates come from
The scaffolds are fetched at runtime over the GitLab API, from Leed’s own repository — project 34227674, path packages/site-management/static/templates/pages, branch main. They are not read from the copy of @leed-ai/cli you have installed.
Two consequences follow. What you get is whatever is on main at the moment you create the page type, so two page types created six months apart can be scaffolded from different versions of the same template. And pinning your CLI version does not pin your scaffolds; the CLI is not involved in this path at all.
Nothing is ever overwritten
Before every scaffold commit, Leed asks GitLab whether the path already exists on the preview branch (staging). If it does, the file is dropped from the commit. If every file in the batch already exists, no commit is made at all.
That is the guarantee behind the whole page: createNewPageType is safe to call repeatedly, and editing a scaffolded file is safe forever. It also means that if you delete a scaffold and want it back, the way to get it is to copy the shipped template again by hand — recreating the page type will not reinstate it, because a slug another page type already holds is refused with a 409.
One naming rule to know before you go looking for the directory data file. It is named after the last segment of the page type’s slug, not the whole slug: a page type at docs/api/3.7.0 gets src/docs/api/3.7.0/3.7.0.11tydata.json. Eleventy binds a directory data file to its own directory by filename, so interpolating the full slug would produce src/docs/api/3.7.0/docs/api/3.7.0.11tydata.json — bound to an empty phantom directory, and inherited by nothing.
Creating the page type in the first place, and every field on that screen, is at Configuring a Page Type. These commits land on the preview branch, so they reach your preview site before your live one — see Preview Site vs Live Site. Everything else Leed writes into your repository, from logos to managed build scripts, is inventoried at What Leed Writes Into Your Repo.