Global Site Data and the Data Cascade

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.

FileWhat it holdsPublished fromHow templates read it
menu.jsonEvery menu on the site, keyed by menu name, as a nested tree of itemsDesign → MenusThe menu partials, and documentationConfiguration.menus.left resolves a docs menu by its key
pageTypeList.jsonEvery page type keyed by pageTypeId — slug, name, layout, feed and sitemap settings, noRenderSettings → Page TypesRead by eleventyComputed to resolve layout, pageTypeId and sitemapPriority; also by the page-type helpers
userList.jsonAuthor records keyed by userId — name, email, biography, departmentSettings → Team MembersThrough the user helper, from a page’s authors array
labelList.jsonLabels keyed by labelId — name, slug, visibility, series membershipSettings → LabelsThrough the label helper, from a page’s labels array
leedForms.jsonEvery form definition keyed by formId — fields, button text, prototype, callbacksSettings → FormsThe form partial resolves a formId against it
autolink.jsonAn array of { text, pageId } pairsSettings → Search & AutolinksThe build’s auto-linking pass, not a template
searchIndexList.jsonConfigured search indexesSettings → Search & AutolinksThe 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"
}
KeyTypeSet in the CMS atDo you reference it in a template?
siteTitlestringSettings → GeneralYes — it is your <title> suffix and your feed title
siteDescriptionstringSettings → GeneralYes, on pages with no summary of their own
siteLocalestringSettings → GeneralOccasionally — the <html lang> and the feeds read it
dateFormatstring (Day.js pattern)Settings → GeneralYes — pass it to the date helpers
readingWpmnumberSettings → GeneralRarely — the reading-time helper consumes it
faviconpathSettings → General (upload)No — emitted by {{> leed/head }}
logopathSettings → General (upload)Not recommended — see below
ogCard.title / .descriptionstringSettings → GeneralNo — emitted by {{> leed/head }}
paging.size / .minimumnumberSettings → GeneralNo — consumed by the collections system
externalAccounts.*string idsSettings → GeneralYes — your social links and author markup
deployment.public / .previewobjectWritten by the platformNo — read it through the siteUrl, pagesUrl and turnstileKey shortcodes
entitlements.tier / .mcpstring / booleanDerived from your planYes — this is what hasTier and hasFeature read
extendedobjectSettings → API documentationRarely — it carries the extra highlighter languages
timezonestringSettings → GeneralOccasionally — 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.json

Interpolating 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:

KeyDerived fromNotable behavior
layoutlayout in front matter, else the page type’s layoutThrows PageType <id> has no layout defined! when neither exists. An explicit "layout": null is honored and disables layout wrapping
pageIdFront matter, or the item on an OpenAPI paginated setOn an API set, the literal "item" is replaced by the real page id
uniquePageIdpageId plus the pagination statelist-<pageId> for a paginated set, with -<n> for pages after the first, and the layer name folded in for a double-layer set
publishedAt / modifiedAtThe page’s own value, or the last item of a paginated listA list page inherits the date of the last entry it shows
pageTypeIdFront matter, resolving the ###ROOT### sentinel###ROOT### maps to whichever page type has an empty slug (or the legacy root slug)
sitemapPriorityThe page type’s sitemapPriority / labelSiteMapPriorityDefaults 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
permalinkThe page’s own permalinkReturns 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.

LevelSourceScopeWins over
1 (highest)Page front matterThat one pageEverything below
2src/<pageTypeSlug>/<name>.11tydata.jsonEvery page of that page typeCompany settings and globals
3src/src.11tydata.jsonEvery page on the siteGlobals
4 (lowest)src/_data/*.jsonEvery page on the siteNothing
—eleventyComputed.jsApplied over the merged resultNot 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/&lt;pageType&gt;/&lt;name&gt;.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 post

A 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.

ESC