A template in your repository reads three kinds of data: the front matter on the page being rendered, the settings attached to that page’s page type, and the site-wide globals under src/_data/. All three are generated by the CMS, all three are committed to your repository, and all three are read-only. This page is the map of where each value comes from, which file survives a round trip, and which value wins when two of them disagree.
Nothing here is plan-gated. Every file on this page is written into every repository on every plan, including Free.
The _data directory
src/_data/ holds the site-wide globals. Every file in it is a projection of CMS state — the CMS builds the JSON, commits it when you publish the matching settings change, and rebuilds it from scratch the next time. Nothing reads your copy back. Changing labelList.json in your editor changes your local build and nothing else; change the label in the CMS, publish, and pull.
| File | What it holds | Published from | How templates read it |
|---|---|---|---|
menu.json | Every menu on the site, keyed by menu name, as a nested tree of items | Design → Menus | The menu partials, and documentationConfiguration.menus.left resolves a docs menu by its key |
pageTypeList.json | Every page type keyed by pageTypeId — slug, name, layout, feed and sitemap settings, noRender | Settings → Page Types | Read by eleventyComputed to resolve layout, pageTypeId and sitemapPriority; also by the page-type helpers |
userList.json | Author records keyed by userId — name, email, biography, department | Settings → Team Members | Through the user helper, from a page’s authors array |
labelList.json | Labels keyed by labelId — name, slug, visibility, series membership | Settings → Labels | Through the label helper, from a page’s labels array |
leedForms.json | Every form definition keyed by formId — fields, button text, prototype, callbacks | Settings → Forms | The form partial resolves a formId against it |
autolink.json | An array of { text, pageId } pairs | Settings → Search & Autolinks | The build’s auto-linking pass, not a template |
searchIndexList.json | Configured search indexes | Settings → Search & Autolinks | The search components |
Two footnotes on that table, both worth knowing before you go looking for a file that is not there.
searchIndexList.json is written by a commit action but only exists once you have created a search index. A repository with no index configured has no such file, and that is normal rather than broken.
ctaList.json ships in the provisioning scaffold and you will find it in most repositories, but it is legacy and no longer written. Dynamic CTAs are served live from the CMS API and stopped producing a ctaList.json commit action; the file in your repository is a fossil of an older release. Do not build a template against it.
The user and label helpers exist precisely so you do not index into these files by hand — the collection and lookup helpers take an id and give you the record. menu.json is the built form of the menus you assemble in the CMS, and leedForms.json is what the form partial resolves a formId against.
Editing them anyway, on purpose
There is one legitimate reason to touch a file in src/_data/: prototyping a template against data that does not exist yet. If you are building a menu partial and the menu has three items, temporarily adding twenty of them to menu.json is a faster way to find your overflow bug than creating twenty real menu items in the CMS.
Do it, then undo it. git restore src/_data/menu.json puts the file back, and so does leed site validate --reset. You cannot ship the experiment by accident — src/_data/**/* is a protected path, so leed site commit refuses while the change is in your working tree. What reset does to each kind of change is worth reading before you use it in anger.
Company-level settings — src/src.11tydata.json
This is the site-wide settings file, and it is an Eleventy directory data file bound to src/, so every page on the site inherits it. It is written from your company record whenever company settings are published.
{
"entitlements": { "tier": "enterprise", "mcp": true },
"externalAccounts": {
"linkedinId": "leed-ai",
"twitterId": "@Leed_AI",
"blueskyId": "leed.ai"
},
"dateFormat": "MMM D, YYYY h:mma z",
"deployment": {
"public": {
"pagesProjectId": "37753414cc134ae3b3ca439cffb3b2bd",
"pagesProjectName": "production-leed-ai",
"domain": "leed.ai",
"pagesDomain": "production-leed-ai.leed.workers.dev",
"turnstilePublicKey": "0x4AAAAAAAUaeZ12nR6l1fIZ",
"customDomainActive": true
},
"preview": { "…": "the same shape for the preview site" }
},
"favicon": "/static/images/favicon-2026-07-13T18-17-42-980Z.svg",
"logo": "/static/images/logo-2026-07-11T15-12-07-709Z.png",
"ogCard": { "title": "Leed", "description": "Docs as a Service" },
"paging": { "size": 9, "minimum": 6 },
"readingWpm": 250,
"siteDescription": "Leed brings together your marketing workflows, website, and customer journey…",
"siteLocale": "en-us",
"siteTitle": "Leed"
}| Key | Type | Set in the CMS at | Do you reference it in a template? |
|---|---|---|---|
siteTitle | string | Settings → General | Yes — it is your <title> suffix and your feed title |
siteDescription | string | Settings → General | Yes, on pages with no summary of their own |
siteLocale | string | Settings → General | Occasionally — the <html lang> and the feeds read it |
dateFormat | string (Day.js pattern) | Settings → General | Yes — pass it to the date helpers |
readingWpm | number | Settings → General | Rarely — the reading-time helper consumes it |
favicon | path | Settings → General (upload) | No — emitted by {{> leed/head }} |
logo | path | Settings → General (upload) | Not recommended — see below |
ogCard.title / .description | string | Settings → General | No — emitted by {{> leed/head }} |
paging.size / .minimum | number | Settings → General | No — consumed by the collections system |
externalAccounts.* | string ids | Settings → General | Yes — your social links and author markup |
deployment.public / .preview | object | Written by the platform | No — read it through the siteUrl, pagesUrl and turnstileKey shortcodes |
entitlements.tier / .mcp | string / boolean | Derived from your plan | Yes — this is what hasTier and hasFeature read |
extended | object | Settings → API documentation | Rarely — it carries the extra highlighter languages |
timezone | string | Settings → General | Occasionally — present only when a timezone is set on the company |
The four groups you do not need to touch
favicon and ogCard are already emitted for you by {{> leed/head }} — referencing them again produces duplicate tags. paging is read by the collections system when it builds a paginated set, so changing it changes your list pages without any template edit. deployment is plumbing: reach for the siteUrl, pagesUrl and turnstileKey shortcodes rather than indexing into it, because they already know which of the two blocks applies to the build in progress.
logo is administrative, not a design token
logo holds exactly one path, and a real site needs at least four logo treatments — header, footer, light and dark. One value cannot serve four contexts. Put your own artwork in src/static/images/ and reference it directly from your templates; leave logo to the CMS, which uses it for the organization record and for places outside your templates entirely.
entitlements is your plan, baked in at build time
entitlements.tier is one of free, starter, growth or enterprise, and entitlements.mcp is a boolean. These are the two values the tier-gating helpers read when you wrap a block of template in {{#if (hasTier "starter")}}. They are baked into the file at publish time, which means a plan change reaches your templates on the next settings publish and the next build — not instantly.
This file is rewritten whole, and that is why a hand-added key never sticks
The observable tell is that the file in a healthy repository contains exactly the keys in the table above and nothing else. If you are looking at a src.11tydata.json with an extra top-level block in it, you are looking at a key on borrowed time.
Page-type settings — src/<pageTypeSlug>/<name>.11tydata.json
Each page type gets one Eleventy directory data file, written from the page type record when the page type is published. It is bound to the folder its pages live in, so every page of that type inherits it and no page of any other type does.
The naming rule looks fussy and is not optional. Eleventy binds a directory data file to its own directory, and the file must be named after that directory — so the file name is the last segment of the page type slug, not the whole slug:
page type slug: blog → src/blog/blog.11tydata.json
page type slug: docs → src/docs/docs.11tydata.json
page type slug: docs/api/3.7.0 → src/docs/api/3.7.0/3.7.0.11tydata.jsonInterpolating the whole slug would produce src/docs/api/3.7.0/docs/api/3.7.0.11tydata.json — a data file bound to a phantom directory, and a page set that inherits nothing.
The contents are three keys: dateFormat, ogCard and documentationConfiguration. That is the entire schema. Anything else you find in one of these files was put there by hand and is not part of the round trip.
An empty {} is normal — and for a documentation set it is fatal
A page type with no per-type overrides ships a file containing exactly {}. For a blog that is correct and harmless: the page type inherits dateFormat and ogCard from the company level, which is what you want.
Which file a documentation configuration survives in
The two 11tydata files are not interchangeable, and the difference is durability rather than precedence.
src/src.11tydata.json is written from a 13-key company schema that does not include documentationConfiguration. A block added there by hand is stripped on the next company settings save.
src/<pageTypeSlug>/<name>.11tydata.json is written from a three-key page-type schema that does include documentationConfiguration, so a value set on the page type survives every publish of that page type indefinitely.
The rule that follows is short: set documentation configuration on the page type. Because the cascade deep-merges the two files key by key, a value that genuinely belongs site-wide can still be set at company level and still reach docs pages — but only for as long as a CMS field keeps writing it there.
The computed layer
src/_data/eleventyComputed.js is not yours and is not in your git history. It is copied into src/_data/ at the start of every build and deleted at cleanup. It runs after the whole cascade has been merged, and it derives seven things:
| Key | Derived from | Notable behavior |
|---|---|---|
layout | layout in front matter, else the page type’s layout | Throws PageType <id> has no layout defined! when neither exists. An explicit "layout": null is honored and disables layout wrapping |
pageId | Front matter, or the item on an OpenAPI paginated set | On an API set, the literal "item" is replaced by the real page id |
uniquePageId | pageId plus the pagination state | list-<pageId> for a paginated set, with -<n> for pages after the first, and the layer name folded in for a double-layer set |
publishedAt / modifiedAt | The page’s own value, or the last item of a paginated list | A list page inherits the date of the last entry it shows |
pageTypeId | Front matter, resolving the ###ROOT### sentinel | ###ROOT### maps to whichever page type has an empty slug (or the legacy root slug) |
sitemapPriority | The page type’s sitemapPriority / labelSiteMapPriority | Defaults to 0.99 for pages and 0.79 for label sets; throws Fix your pageType in the file referenced by: <slug> when the front matter names a page type that does not exist |
permalink | The page’s own permalink | Returns false — meaning do not render — when previewOnly is set on a public build, or when the page type is noRender and the template did not pass forceRender |
Two edge cases worth knowing before you debug one
The sitemapPriority throw is the friendliest error in the build, because it names the file. If you copy a page file between repositories and forget to update pageTypeId, this is the error you get, and the fix is in the message.
layout returning undefined is deliberate, not a bug. A noRender page type returns undefined from the layout resolver on purpose: its pages are never rendered directly, so wrapping them in a layout would emit an empty document. A template that paginates over such a set supplies its own output path instead.
The precedence chain
Highest wins. This is Eleventy’s own data cascade, with eleventyComputed bolted on at the end.
| Level | Source | Scope | Wins over |
|---|---|---|---|
| 1 (highest) | Page front matter | That one page | Everything below |
| 2 | src/<pageTypeSlug>/<name>.11tydata.json | Every page of that page type | Company settings and globals |
| 3 | src/src.11tydata.json | Every page on the site | Globals |
| 4 (lowest) | src/_data/*.json | Every page on the site | Nothing |
| — | eleventyComputed.js | Applied over the merged result | Not a level — a transform |
The eleventyComputed row is the one readers get wrong, and it is why this is drawn rather than listed. It is not a fifth precedence level sitting above front matter. It runs after levels 1–4 have already merged into one object, reads that object, and writes derived keys back over it. That is why layout can be resolved from pageTypeId even though pageTypeId came from front matter and the layout came from a global file.
flowchart TB
subgraph merge["The data cascade — merged deepest-first"]
direction TB
G["<b>src/_data/*.json</b><br/>site-wide globals<br/><i>lowest precedence</i>"]
C["<b>src/src.11tydata.json</b><br/>company settings"]
P["<b>src/<pageType>/<name>.11tydata.json</b><br/>page-type settings"]
F["<b>page front matter</b><br/><i>highest precedence</i>"]
G --> C --> P --> F
end
F --> M["one merged data object"]
M --> E["<b>eleventyComputed.js</b><br/>reads the merged object,<br/>writes layout, pageId, uniquePageId,<br/>dates, pageTypeId, sitemapPriority, permalink"]
E --> R["the data your template sees"]
A worked example. Say dateFormat is set three times:
src/_data/… (not set — dateFormat is not a global)
src/src.11tydata.json "MMM D, YYYY h:mma z" ← company default
src/blog/blog.11tydata.json "MMMM D, YYYY" ← blog pages only
src/blog/launch-week.md "dddd" ← this one postA page under src/platform/ gets MMM D, YYYY h:mma z. Every blog post except one gets MMMM D, YYYY. launch-week.md gets dddd. No template changes; the cascade does all of it.
Deeper wins key by key, not whole-object
This is the half of the merge that surprises people. Two files that both carry a documentationConfiguration object do not replace one another. The deeper file’s keys win individually, and the shallower file’s other keys survive underneath.
That is exactly what lets a site set menus.top and menus.bottom at company level while the docs folder adds only menus.left — and a docs page ends up with all three:
// src/src.11tydata.json
{ "documentationConfiguration": { "menus": { "top": "87cf4837", "bottom": "IU24ZTE1" } } }
// src/docs/docs.11tydata.json
{ "documentationConfiguration": { "menus": { "left": "leeddocs" } } }
// what a page under src/docs/ actually sees
{ "documentationConfiguration": { "menus": {
"top": "87cf4837", "bottom": "IU24ZTE1", "left": "leeddocs" } } }Files that appear only during a build
Three things land in your working tree during a build and are gone when it finishes: src/_data/eleventyComputed.js, src/_data/leed-form-data.json (the country list the form partial renders), and the whole leed-* template and asset set. All of them are gitignored, and all of them are removed when the build ends or the dev server is stopped.
This is why git status can look completely clean straight after a build that obviously wrote files — and why you should never create a file of your own at a path matching one of those reserved patterns. The complete reserved-pattern list, and what the build sweeps is on the static-files page.
Each generated file on this page arrives through a publish, and the full inventory of what Leed writes into your repository sorts them by which wave puts them there. The layer above all of this — the front matter on the page itself — is documented in the front matter reference.