Front Matter Reference

Every page file in your repository opens with a block between --- fences. CMS-written .md pages and hand-authored .hbs pages both have one, and in both cases the block is JSON, not YAML.

That distinction is the first thing to internalize, because it fails badly. A trailing comma, an unquoted key or a single-quoted string is not front matter with a warning — it is a build error. And nothing catches it before then: leed site validate does not parse front matter at all, so a malformed block passes validation cleanly and then breaks the build. Write it as JSON, and let your editor’s JSON linter do the work.

The shape

The CMS serializes a published page as the front-matter fences, a two-space-indented JSON object, and the page body:

---
{
  "authors": [
    "usvc"
  ],
  "disableCta": false,
  "disableAutolink": false,
  "labels": [
    "pcyk31"
  ],
  "keywords": [
    "Content Management",
    "Personalization"
  ],
  "pageId": "ca9c8ebb-89cd-4426-b748-dc4cda9fa8f8",
  "pageTypeId": "100111",
  "title": "6 Top Reasons to Switch",
  "slug": "6-top-reasons-to-switch",
  "summary": "Businesses striving to stay ahead …",
  "wordCount": 916,
  "publishedAt": "2024-12-18T21:15:00.000Z",
  "modifiedAt": "2024-12-18T21:05:37.359Z",
  "previewOnly": false,
  "eleventyExcludeFromCollections": false,
  "aliases": []
}
---
Body content starts here.

Keys appear in schema order, and the whole block is rewritten wholesale on every publish. Hand-editing a CMS-written page file is therefore pointless as well as blocked — your change survives until the next publish of that page and is then replaced by the serializer’s output. Page-type folders are read-only for exactly this reason; see Editable and Read-Only Files.

If you are here to write one file by hand, this table is the whole answer:

KeyExampleWhat breaks without it
title"Compare Leed and Everything Else"No <title>, no heading for templates that render it, nothing usable in listings
pageTypeId"110011"No layout can be resolved — the build throws
pageId"6f0f9d5e-2a71-4a24-8d2f-4e6d1b4a9c07"pageid: links to the page cannot resolve; analytics cannot attribute it
publishedAt"2026-09-01T12:00:00.000Z"Sorts arbitrarily in collections, empty <lastmod>, empty <pubDate>, invalid JSON-LD
modifiedAt"2026-09-01T12:00:00.000Z"Empty Atom <updated>, empty article:modified_time, no dateModified
summary"A feature-by-feature comparison."No <meta name="description">, blank card and search-result blurbs — and a documentation page cannot be published at all

The rest of this page is the complete reference.

Keys the CMS writes

Twenty-one keys, in schema order. Everything a published page carries is here — there is no hidden set.

Identity

pageId, pageTypeId, title and slug. pageId is the page’s permanent identity and is what pageid: links, analytics and the search index all key on. slug is the last segment of the URL and is derived by the CMS from the title, not chosen independently — renaming a page changes its URL.

Metadata and SEO

summary, keywords, ogCard, featureImage, featureVideo and wordCount. summary becomes the meta description and the blurb on cards and search results. ogCard is a {title, description, image} object for social previews. featureImage and featureVideo are asset references — {assetId, src, width, height, alt, svg} for an image — so a template gets everything it needs to render responsive markup without a second lookup. wordCount is computed on save and is what reading-time estimates are built from.

Relationships

authors and labels hold ids, not names — resolve them through the collection-lookup helpers rather than printing them raw. formId names the form a layout should render, which is how Placing a Form on a Page works. aliases is an array of full paths that should redirect to this page; it is always emitted, as [] when there are none, and it is how an old URL keeps working after a slug change — see Aliases and Redirects.

Dates

publishedAt, modifiedAt and plannedDate. The format is ISO-8601 UTC with milliseconds and a trailing Z — "2026-09-01T12:00:00.000Z" — because that is what JSON.stringify produces for a Date. A date-only YYYY-MM-DD never appears in this block, and you should not write one.

Writers guess wrong about which date does what, so here it is explicitly:

FieldRead by
publishedAtCollection sort order; sitemap <lastmod>; RSS <pubDate>; Atom <published>; JSON Feed date_published; JSON-LD datePublished
modifiedAtAtom <updated>; JSON Feed date_modified; JSON-LD dateModified; the article:modified_time OG meta tag
plannedDateThe editorial calendar and scheduling; display order in planning views

Listing templates sort on publishedAt, and the comparison falls back to 0 when the value is missing — so every undated page compares equal to every other undated page and their relative order is decided by URL, not by anything you intended.

Neither date hides a page. Visibility is a build-inclusion question, not a date question: a publishedAt in the future does not hold a page back once it is in the repository.

Render switches

KeyDefaultWhat it does
eleventyExcludeFromCollectionsfalseKeeps the page out of every Eleventy collection — no sitemap entry, no feed entry, no listing page
disableAutolinkfalseSuppresses auto-linking inside this page’s body, leaving the text exactly as written
disableCtafalseMakes the CTA shortcode emit nothing on this page, so no dynamic call-to-action is injected
previewOnlyfalseExcludes the page from production builds. Written by Leed — not a control you set

previewOnly is in this table because you will see it in every CMS-written file, not because it is a switch you should reach for. It has no CMS surface, it defaults to false, and setting it by hand on a page of your own is a quiet way to make that page disappear from your live site while it continues to work in preview. Leave it out of files you author.

disableAutolink and disableCta switch off the two site-wide injections, documented on Autolinks and Dynamic CTAs respectively.

The complete reference

KeyTypeAlways present?Written fromWhat the build does with it
pageIdstringYesMinted by the CMS on createKeys pageid: link resolution, analytics, the search index
pageTypeIdstringYesThe page type chosen on createResolves the layout and the sitemap priority
titlestring, 2–150 charsYesPage editor<title>, headings, listing entries; the CMS derives slug from it
sluglowercase, dash-separatedYesDerived from titleThe last segment of the URL
summarystringYes (may be "")Page settingsMeta description, card and search-result blurb
keywordsstring arrayWhen setPage settingsKeyword meta tags
authorsstring array of user idsWhen setPage settingsBylines, via the user lookup helper
labelsstring array of label idsWhen setPage settingsLabel pages, filtering, RBAC overrides
featureImageasset reference objectWhen setPage settingsHero image and social card fallback
featureVideoasset reference objectWhen setPage settingsHero video
ogCard{title, description, image}When setPage settingsOpen Graph and Twitter card tags
formIdstringWhen setPage settingsThe form the form partial renders
wordCountnumberWhen setComputed on saveReading-time estimates
publishedAtISO-8601 UTCOn every published pagePublish action or the scheduleCollection sort, sitemap, all three feeds, JSON-LD
modifiedAtISO-8601 UTCYesEvery saveAtom <updated>, JSON Feed date_modified, JSON-LD, OG modified time
plannedDateISO-8601 UTCWhen setPlan calendarEditorial scheduling and planning order
previewOnlybooleanYesLeedExcludes the page from production builds
eleventyExcludeFromCollectionsbooleanYesPage settingsRemoves the page from every collection
disableAutolinkbooleanYesPage settingsSkips auto-linking on this page
disableCtabooleanYesPage settingsSuppresses the CTA shortcode on this page
aliasesstring array of pathsYes ([] when empty)Path historyGenerates redirects to this page
passthrough keysanythingWhen presentImport pathsRe-emitted verbatim at the top level

That last row is extraFrontmatter: a passthrough channel Leed uses to carry keys it does not model, such as the flattened OpenAPI blob on generated API pages and menuId. It is written by import paths, not by you, and it is emitted first, so a modeled key always wins on a collision — nothing carried through can overwrite title or slug.

A CMS-written page with every optional key populated
{
  "authors": ["usvc", "b93t"],
  "disableCta": false,
  "disableAutolink": false,
  "labels": ["pcyk31"],
  "keywords": ["Content Management", "Personalization"],
  "featureImage": {
    "assetId": "zrqm1y5l",
    "src": "/cdn-cgi/imagedelivery/CkSmQAGqpZ-mcWDDI6mu0w/e4feea80-1c0d/original",
    "width": 2100,
    "height": 1500,
    "alt": "A large orange number six in a modern office setting.",
    "svg": false
  },
  "featureVideo": { "assetId": "b21kd0aa", "videoId": "9f2c…" },
  "ogCard": {
    "title": "6 Top Reasons to Switch",
    "description": "Why teams move to a generative CMS.",
    "image": "/static/images/og/switch.png"
  },
  "pageId": "ca9c8ebb-89cd-4426-b748-dc4cda9fa8f8",
  "pageTypeId": "100111",
  "title": "6 Top Reasons to Switch",
  "slug": "6-top-reasons-to-switch",
  "summary": "Businesses striving to stay ahead of the competition …",
  "wordCount": 916,
  "formId": "resource-download",
  "publishedAt": "2024-12-18T21:15:00.000Z",
  "plannedDate": "2024-12-15T00:00:00.000Z",
  "modifiedAt": "2024-12-18T21:05:37.359Z",
  "previewOnly": false,
  "eleventyExcludeFromCollections": false,
  "aliases": ["/blog/six-reasons-to-switch/"]
}

These keys land in a file only when a page is published. Nothing about a draft appears in your repository — see How Publishing Works.

Keys your own pages must supply

Six: title, pageTypeId, pageId, publishedAt, modifiedAt, summary.

That is not a guess or a recommendation. It is what all 361 pages of a shipped Leed documentation site carry, and what the seed fixture’s pages carry. Everything else is optional and does something specific when present.

---
{
  "title": "Compare Leed and Everything Else",
  "pageTypeId": "110011",
  "pageId": "6f0f9d5e-2a71-4a24-8d2f-4e6d1b4a9c07",
  "publishedAt": "2026-09-01T12:00:00.000Z",
  "modifiedAt": "2026-09-01T12:00:00.000Z",
  "summary": "A feature-by-feature comparison of Leed against the alternatives."
}
---
KeyRequired?ExampleWhat breaks without it
titleYes"Compare Leed and Everything Else"Empty <title> and empty listing entries
pageTypeIdYes"110011" or "###ROOT###"The build throws — no layout can be resolved
pageIdYesa UUID v4pageid: links to the page do not resolve; no analytics attribution
publishedAtIn practice"2026-09-01T12:00:00.000Z"Arbitrary sort order, empty <lastmod> and <pubDate>, invalid JSON-LD
modifiedAtIn practice"2026-09-01T12:00:00.000Z"Empty Atom <updated> and article:modified_time
summaryFor documentation page types, yesone sentenceNo meta description; publish is refused for a documentation page
layoutNo"blog.hbs" or nullFalls back to the page type’s layout
permalinkNo"404.html"Eleventy’s default URL from the file path is used
keywordsNo["comparison"]Nothing; keyword meta tags are simply absent

There is no category key and no order key in Leed front matter. Neither exists in any schema. Nesting comes from where the file sits under src/; ordering in a documentation set comes from the menu. If you have seen a “five keys: title, summary, keywords, category, order” description of Leed front matter, it is wrong.

Why the two dates are effectively required

Neither date is enforced. publishedAt is nullable in the database and the build will not stop without it. But an undated page is a page that behaves badly everywhere it is aggregated: it sorts arbitrarily against every other undated page, its sitemap <lastmod> is empty, its feed entries have empty dates, and its JSON-LD datePublished is invalid — which is a structured-data error a search engine will report back to you.

The convention worth adopting: set publishedAt to a clean round hour on the day the page goes live, keep modifiedAt greater than or equal to it, and bump modifiedAt when you substantively change the page. modifiedAt is non-null in the revision table and is always present on a CMS-written file, so a hand-authored page without one is the odd file out.

summary is optional in the schema and required at publish

For a documentation page type, requiredFields.summary defaults to true, and the publish action enforces it: a page without a summary is refused with a 400 carrying "Missing required fields" and the list of what is missing. posts page types default to requiring summary, featureImage and keywords; api page types require none of them.

requiredFields is a per-page-type setting, not a global rule, and it is editable — see Configuring a Page Type. Either way, treat summary as required for real reasons rather than validation ones: it is the <meta name="description">, the card blurb and the search-result blurb, so an empty one is a visible loss, not just a failed check.

There is no path or URL key

Nothing in front matter sets a page’s address. Not path, not url, not category. A page’s URL comes entirely from where its file sits under src/, and that mapping is a hard invariant rather than a convention. It has its own page: Page Paths and Folder Structure.

permalink is the one exception, and it is Eleventy’s key rather than Leed’s: "permalink": "404.html" is how the scaffold’s 404 page lands at a fixed filename. Use it for genuine special cases, not for routing ordinary pages.

Every pageId in the build must be unique

The build assembles a map from pageId to rendered path, and every pageid: link in the site is resolved through it. A map has one entry per key, so two pages sharing a pageId means every link to either of them lands on whichever rendered last — silently, with no warning and no build failure. It is a bug you find by clicking, weeks later.

Generate a real UUID v4 for each hand-authored page. Never copy one from another file, and never leave a placeholder in. (Leed’s own scaffold uses the literal "404" for the 404 page, which is unique and never linked to — that is the exception that proves the rule, not a pattern to copy.)

A pageid: link whose target does not exist in the map at all is handled differently and more visibly: the anchor is replaced by a <span class="page-removed"> carrying the link text. If you see one of those in a built page, the target was never published.

The ###ROOT### sentinel

Root-level pages — the ones that live directly under src/ rather than in a page-type folder — belong to a page type whose slug is empty. Its id differs per site, so hard-coding it makes a template unportable. Write the sentinel instead:

{ "pageTypeId": "###ROOT###" }

At build time that resolves to whichever page type has an empty slug, or the legacy slug root. Leed’s own portable templates use it for exactly this reason.

If no such page type exists, the sentinel does not resolve, the lookup misses, and the build fails with PageType ###ROOT### has no layout defined! — which is a confusing message for the real problem, namely that the site has no root page type at all.

uniquePageId is computed for you

Paginated listing pages render many URLs from one source file, so they need an identifier per rendered page rather than per source file. uniquePageId is that identifier. You supply pageId; the build derives the rest.

Page kindFormatExample
Not paginatedthe pageId itself6f0f9d5e-2a71-…
Single-layer paginated (:paged)list-<pageId> on the final page, list-<pageId>-<n> while a next page existslist-6f0f9d5e-…-1
Double-layer paginated (:labels, :authors)list-<pageId>-<listName>, with -<n> on non-final pageslist-6f0f9d5e-…-tutorials-2

Collections and the pagination data that produces these are on Collections and Pagination Data.

Your own keys

Anything else you put in the block is available in the template. This — arbitrary front-matter keys — is what “custom fields” means in Leed. There is no customFields schema field to configure and nothing to register first; you write a key, you read it in the template.

---
{
  "title": "Pricing",
  "pageTypeId": "###ROOT###",
  "pageId": "0d8b0f1a-59f0-4a4a-9f0e-2f6e4a2b1c33",
  "publishedAt": "2026-09-01T12:00:00.000Z",
  "modifiedAt": "2026-09-01T12:00:00.000Z",
  "summary": "Plans and pricing.",
  "heading": "Simple pricing",
  "subtitle": "Start free. Upgrade when you outgrow it.",
  "items": [
    { "name": "Free", "price": "$0" },
    { "name": "Starter", "price": "$250" }
  ]
}
---
<h1>{{ heading }}</h1>
<p>{{ subtitle }}</p>
<ul>
  {{#each items}}
    <li>{{ this.name }} — {{ this.price }}</li>
  {{/each}}
</ul>

Front matter is the highest-precedence layer of the Eleventy data cascade, so a key you set here also overrides the same key set for the page type or the whole site. What sits below it is on Global Site Data and the Data Cascade.

Choosing a layout from front matter

Layout resolution has three possible outcomes, one of which is a hard build failure, and readers hit that failure without knowing which branch produced it:

flowchart TD
  A["Render a page"] --> B{"layout set in front matter?"}
  B -- "an explicit value" --> L1(["Use that layout"])
  B -- "explicitly null" --> L2(["Render with no layout wrapper"])
  B -- "not set" --> C["Resolve pageTypeId,<br/>mapping the root sentinel"]
  C --> D{"Page type found?"}
  D -- no --> T(["THROW: PageType has no layout defined!"])
  D -- yes --> E{"Page type is noRender?"}
  E -- yes --> L3(["No layout — the page type<br/>opts out of rendering"])
  E -- no --> F{"Page type has a layout?"}
  F -- yes --> L4(["Use the page type's layout"])
  F -- no --> T

In prose: an explicit layout wins. "layout": null disables layout wrapping entirely, so the page renders its own complete document — that is what the scaffold’s index.hbs does. With no layout key at all, the page type’s configured layout is used. A page type marked noRender deliberately returns nothing here, because a template elsewhere decides when and where its pages are rendered. And a page type with no layout configured is a build failure, not a fallback.

Which layout a page type is bound to is set when you configure it — see Layouts and Page Types.

Computed keys you never write

Three more values are computed during the build and then behave as if they had been in your front matter all along.

permalink decides whether a page is rendered at all. It returns false — meaning do not render — when previewOnly is set and the build is not a preview build, and when the page type is noRender without an explicit forceRender. Otherwise it passes your own permalink through. This is the mechanism behind previewOnly: the page is not hidden after rendering, it is never rendered.

sitemapPriority comes from the page type rather than the page: the page type’s configured priority, defaulting to 0.99, or its label-page priority, defaulting to 0.79, for a label-paginated set. A page whose pageTypeId does not match any known page type fails here with Fix your pageType in the file referenced by: and the filename — usually the fastest way to find a typo’d page type id.

The date rollup on list pages. A paginated listing page has no publish date of its own, so when its publishedAt or modifiedAt matches the pagination alias, the build substitutes the date of the last item in the paginated set. That is what stops a blog index from appearing in the sitemap with an empty <lastmod>.

ESC