One partial supplies everything a Leed page’s <head> needs: the stylesheet, the metadata behind your social cards, the page-identity constants the trackers read, and every script the site runs on. You call it once, inside your own <head>, and you supply the <title> yourself.
<head>
<title>{{ title }} | {{ siteTitle }}</title>
<meta name="viewport" content="width=device-width">
{{> leed/head }}
</head>Alongside it, three components are meant to be called from your templates: a drop-in pagination control, a reading-progress bar, and a modal video player. Everything else under leed/ is internal — the full map of what Leed injects is the Leed partial index.
leed/head
The partial emits in file order, and that order is the load order in the browser. Every row below is conditional on something, so a missing tag is nearly always a missing input rather than a bug.
| Output | Condition | Read from |
|---|---|---|
{{> header-includes }} | header-includes.hbs exists in src/_includes/ | your file |
<link rel="preload" as="style"> + <link rel="stylesheet"> | the site is built with Tailwind | the hashed stylesheet path |
leed/metadata/siteMetadata | always | site and page fields |
leed/metadata/twitterCard | always | site and page fields |
leed/metadata/ogCard | always | site and page fields |
{{{ JSON-LD }}} | always called; emits a script only for the blog page type | page type, page and author data |
const pid / const pt | always | uniquePageId, pageTypeId |
searchIndexId, searchIndexHash, searchDocumentsHash | Lunr search only, and only when the docs set has a search index id | documentationConfiguration.searchIndexId |
Alpine, js-cookie, zoomable, fingerprint, privacy, timer, tracker | always | cdnjs and your site’s /static/js/ |
webmcp | entitlements.mcp is on | site entitlements |
leed-utils, documentation | always | /static/js/ |
live-search or search | whichever the site’s plan supports | the docsLiveSearch feature |
Turnstile’s api.js | deployment.public.turnstilePublicKey is set | deployment config |
{{> leed/docs/style }} | documentationConfiguration.logo is set | the docs logo paths |
Your own tags come first
Line 1 is a templateExists "header-includes.hbs" probe. If that file is present in src/_includes/, it is included before anything Leed emits — favicons, a web manifest, theme-color metas, verification tags, a font preconnect. It needs no flag and no CMS setting; creating the file is what turns it on. Its full treatment, including why it is the only hook that reaches both marketing pages and documentation pages, is at Customizing the <head>.
Stylesheet
The Tailwind sheet is emitted as two tags — a <link rel="preload" as="style"> and a plain <link rel="stylesheet"> — not the combined rel="preload stylesheet" you may be used to.
Metadata
Three partials run in order: leed/metadata/siteMetadata, then leed/metadata/twitterCard, then leed/metadata/ogCard. If a social card comes out wrong, these tables are how you trace the tag back to the field that fed it.
leed/metadata/siteMetadata
| Tag emitted | Value read from | Fallback |
|---|---|---|
<link rel="sitemap"> and the JSON/RSS/Atom feed links | siteTitle, siteUrl | none |
<link rel="icon"> | favicon | omitted when unset |
<meta name="title"> | title | none |
<meta name="keywords"> | keywords | omitted when unset |
<meta name="description"> | summary | siteDescription |
<link rel="canonical"> | canonicalUrl | the page’s own URL; suppressed entirely on stub pages |
<meta property="published"> | publishedAt, ISO-formatted | none |
leed/metadata/ogCard
| Tag emitted | Value read from | Fallback |
|---|---|---|
og:locale, og:site_name, og:site | siteLocale, siteTitle, siteUrl | none |
og:title | title | ogCard.title |
og:description | summary | ogCard.description |
og:image | featureImage | ogCard.image |
og:image:width / og:image:height | ogCard.imageWidth / ogCard.imageHeight | 800 / 413 |
og:url | the page URL | none |
og:type plus the article:* tags | article when the page type is in feeds | website |
leed/metadata/twitterCard
| Tag emitted | Value read from | Fallback |
|---|---|---|
twitter:card | ogCard.cardType | summary |
twitter:url | the page URL | none |
twitter:site | twitterId | omitted when unset |
twitter:image | featureImage | ogCard.image, then logo |
twitter:title | title | ogCard.title |
twitter:description | summary | omitted when unset |
twitter:label1 / twitter:data1 | reading time, when wordCount is set | omitted when unset |
The og:image and twitter:image values go through the Cloudflare Images social variant, which is why the default 800 × 413 dimensions are stated: they describe that variant. A site supplying its own ogCard.image is not going through it and should set its own width and height, or the two tags describe someone else’s file.
Immediately after the three partials, leed/head calls the JSON-LD shortcode. It emits a real <script type="application/ld+json"> block only for pages whose page type slug is blog — a BlogPosting, or Blog for a paginated list page, with the visible authors attached. Every other page type gets an HTML comment saying structured data was not generated for it. What those tags and that block mean to a crawler is covered at Social Cards and Structured Data; the site title, description, logo and social handles they all read are set on Site Identity and Branding.
Inline page identity
Two constants are written into an inline <script> on every page:
<script>
const pid = "list-bd9be3d0";
const pt = "u05nxr";
</script>pid is the page’s uniquePageId and pt is its page type id. The analytics tracker reads them to attribute a pageview, and the search client reads pt to scope a query to the right documentation set. On the Lunr search path only, three more constants join them — searchIndexId, searchIndexHash and searchDocumentsHash — because a prebuilt index is a build-time artifact that has to be matched to the page that loads it.
Scripts
Alpine and js-cookie come from cdnjs at pinned versions (currently Alpine 3.16.3 and js-cookie 3.0.8); everything else is served from your own site under /static/js/ with a content hash in the filename. The order matters: leed-utils and documentation register the Alpine components that the markup further down the page binds to, so nothing you write should load Alpine a second time.
webmcp.js is the reader-facing site agent and loads only when the workspace has MCP enabled. MCP is free on every plan — the flag is a workspace switch, not a tier gate. See Site AI Agent.
And finally the docs logo variables
The last thing leed/head emits is {{> leed/docs/style }}, a small <style> block that turns documentationConfiguration.logo into a CSS custom property:
<style>
:root {
--nav-logo-url: url('/static/images/logo/leed-logo-p-light.svg');
@media (prefers-color-scheme: dark) {
--nav-logo-url: url('/static/images/logo/leed-logo-p-dark.svg');
}
}
</style>.nav-logo in the header and .logo-footer in the footer both resolve bg-[image:var(--nav-logo-url)] against it.
The emitted <head> of a real documentation page
<link href="/static/css/tailwind.css?v=8ebca811…" rel="preload" as="style">
<link href="/static/css/tailwind.css?v=8ebca811…" rel="stylesheet">
<link rel="sitemap" title="Leed - Sitemap" type="application/xml" href="https://leed.ai/sitemap.xml">
<link rel="alternate" title="Leed - RSS Feed" type="application/rss+xml" href="https://leed.ai/rss.xml">
<link href="/static/images/favicon-….svg" rel="icon" as="image">
<meta charset="UTF-8">
<meta name="generator" content="Leed Eleventy">
<meta name="title" content="Documentation List">
<meta name="description" content="…">
<link rel="canonical" href="https://leed.ai/docs/">
<meta property="published" content="2026-09-01T12:00:00.000Z">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Documentation List">
<meta property="og:locale" content="en-us">
<meta property="og:site_name" content="Leed">
<meta property="og:url" content="https://leed.ai/docs/">
<meta property="og:type" content="article">
<!-- JSON+LD not generated for docs type of page -->
<script>
const pid = "list-bd9be3d0";
const pt = "u05nxr";
</script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.16.3/cdn.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.8/js.cookie.min.js"></script>
<script src="/static/js/l.zoomable.5.min.js"></script>
<script src="/static/js/l.fp.1.min.js"></script>
<script src="/static/js/l.priv.2.min.js"></script>
<script src="/static/js/l.timer.2.min.js"></script>
<script src="/static/js/l.whisper.24.min.js"></script>
<script defer src="/static/js/l.webmcp.3.min.js"></script>
<script src="/static/js/l.utils.31.min.js"></script>
<script src="/static/js/l.docs.22.min.js"></script>
<script src="/static/js/l.livesearch.2.min.js"></script>
<script defer src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script>
<style>:root { --nav-logo-url: url('…'); }</style>leed/components/pagination
The drop-in list control: a range line, numbered links with ellipsis, and two arrows.
{{> leed/components/pagination }}
{{> leed/components/pagination data=myAlias }} It renders nothing at all when pageCount <= 1, so a list that fits on one page needs no guard around the call. The markup it produces gives you these hooks:
| Hook | What it is |
|---|---|
.pagination-page-range | the “Showing 1 to 10 of 42 results” line |
nav.pagination-list | the numbered-link container |
a.pagination-active | the current page — an <a> with no href and aria-current="page" |
a.pagination-inactive | every other page number, and both arrows |
a.pagination-ellipsis > i.ellipsis | the gap marker in a long run |
span.arrow > i.back-arrow / i.next-arrow | the two arrow glyphs |
The list object this reads, and every collection it can come from, is documented at Collections and Pagination Data; the three helpers underneath it have full signatures at Pagination Helpers.
leed/components/scroll-status-indicator-horizontal
A reading-progress bar for one scrollable element.
{{> leed/components/scroll-status-indicator-horizontal scrollBarId="blog-copy" }}The id names the element whose scroll is tracked, and it is handed straight to the Alpine handler scrollStatusIndicatorHorizontalHandler(id). The outer <div> is the track and the inner one is the fill, and the fill is scaled rather than widened — transform: scaleX() on an origin-left element, recomputed every animation frame. Setting width that often would re-lay-out the bar on every frame of a long article; a transform is composited and stays off the main thread. Do not write CSS that sets a width on the inner element, and leave origin-left alone: those two are what make scaleX(0…1) read as 0–100%.
Its three colors come from --scroll-status-indicator-horizontal-start-bg, -partial-bg and -complete-bg, which are ordinary site tokens — see the Site Token Contract.
leed/popup-video
A button that opens a modal Cloudflare Stream player, dismissed by the backdrop or the Escape key.
{{> leed/popup-video
buttonLabel="Watch the demo"
buttonClass="btn btn-primary"
videoAssetId="abc123"
videoSrc="https://customer-xxxx.cloudflarestream.com/VIDEO_ID/iframe"
data-id="hero-video" }}| Partial | Parameter | Required | Effect |
|---|---|---|---|
components/pagination | data | no | the pagination object to render; defaults to @root/list |
components/scroll-status-indicator-horizontal | scrollBarId | yes | the id of the element to track; without it nothing renders |
popup-video | buttonLabel | yes | button text, emitted raw so an icon or other HTML is allowed |
popup-video | videoAssetId | yes | the asset id, used for the play and pause calls |
popup-video | videoSrc | yes | the Stream iframe URL |
popup-video | buttonClass | no | classes on the trigger button |
popup-video | data-id | no | tracking attribute on the trigger button |
The <iframe> src is bound, not static: it stays empty until the modal opens and is then built by buildUrl videoSrc "autoplay=true", so no page pays for a video nobody clicked. Closing the modal — backdrop, Escape or a click outside the player — calls leedPopupVideoPause(videoAssetId), which is why the asset id is required rather than cosmetic. Stream ids come from the asset library; see Video, Audio and Document Assets.