Content graph
A pre-rendered map of every page's headings, images, links, and excerpts, plus the links between pages. Built once before Eleventy's main build by a programmatic dry-run of the site; cached at .cache/_baseline/content-graph.json. Templates read it through cascade keys; modules read it through coreContext.runtime.contentGraph.
Why it exists
Eleventy's data cascade can read front matter; it cannot read what a page actually rendered. The content graph fills that gap.
A pre-pass renders every page once, extracts the structured content (headings, links, images, excerpt) from the result, and writes it to a queryable record before any consumer template runs.
The pre-pass attaches to Eleventy's eleventy.before event, so the graph rebuilds at the start of every cycle: initial build, watch rebuild, production build. The cached file is the cold-start handoff; in-memory is the hot path.
Where it surfaces
- Cascade keys (per page):
_node,_backlinks,_outgoing. See Globals for shape. - Cascade key (reserved):
_edges, listed inINTERNAL_KEYSbut currently unbound. - Nunjucks global:
_navigatorcarries{ nodes, edges, backlinks }. Registered by the navigator module. - Runtime read: modules read through
coreContext.runtime.contentGraph(a getter). The getter form lets rebuilds reassign the underlying reference without re-registering listeners.
The graph shape
Three top-level keys.
{
nodes: {
"/en/foo/": {
// Identity (from data._pageContext via dataFilterSelectors)
title, slug, description, section, type, lang, locale, date, url,
// Extracted (from the rendered DOM)
excerpt, headings, images
}
},
edges: [
{ internal, from, to, type, text }
// One entry per <a href> in each page's extracted root.
],
backlinks: {
"/en/foo/": [
{ url, title?, excerpt? }
// Pre-joined with the source page's title and excerpt.
]
}
}
Identity fields come from data._pageContext via dataFilterSelectors.add('_pageContext') on the pre-pass Eleventy. Extracted fields come from the rendered DOM. The two halves are merged in the graph builder; each node is a single flat record.
Edges are flat. One per anchor in the extracted root. to is origin-stripped for known origins (the dev server, process.env.URL, settings.url); external URLs are left alone. Not deduplicated.
Reading it in a template
_node.headings for a table of contents
_node.headings is Array<{ level, text, id }>. The first heading is usually the page's <h1>; skip it and render the rest:
{% set toc = _node.headings %}
{% if toc.length %}
<nav class="toc" aria-labelledby="toc-title">
<p id="toc-title">On this page</p>
<ol>
{% for heading in toc.slice(1) %}
<li class="toc-level-{{ heading.level }}">
<a href="#{{ heading.id }}">{{ heading.text }}</a>
</li>
{% endfor %}
</ol>
</nav>
{% endif %}
id is the live DOM id when the rendering pipeline anchored the heading; otherwise a slugified fallback derived from text.
_backlinks for "what links here"
_backlinks carries bare edges ({ internal, from, to, text, rel }), not enriched records. To render with the source page's title and excerpt, group by source URL and look up each source in _navigator.nodes:
{% if _backlinks.length %}
<nav aria-labelledby="backlinks-title">
<h2 id="backlinks-title">Linked from</h2>
<ul>
{% for from, links in _backlinks | groupby('from') %}
{% set source = _navigator.nodes[from] %}
<li>
<a href="{{ from }}">{{ source.title or from }}</a>
{% if source.excerpt %}<p>{{ source.excerpt }}</p>{% endif %}
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
For an enriched lookup without the groupby step, read _navigator.backlinks[page.url] instead. The cross-page map is target-keyed and pre-joined; the per-page key is the local view.
_navigator.backlinks for a site-wide index
Paginate over every target URL to generate /backlinks/<slug>/ subpages. Each entry comes pre-joined with source title and excerpt:
---
pagination:
data: _navigator.backlinks
size: 1
alias: target
permalink: '/backlinks//'
---
Excluding a page from the graph
Two front-matter flags. Both ride the pre-pass via dataFilterSelectors, so the graph builder sees them and skips the page entirely.
_internal: trueis the opt-out for synthetic and internal templates (sitemap XML, schema endpoints, the markdown-sibling renderer). The graph honours it alongside the page-context registry and the head module.baselineExcludeFromGraph: trueis a graph-specific opt-out for pages that render but should stay outside the graph. The canonical use is a view onto the graph (a backlinks index, an outgoing-links index) that should render without self-referencing.
Note that eleventyExcludeFromCollections: true is Eleventy's collections opt-out and does not exclude a page from the graph on its own. A page can stay out of nav, listings, and the sitemap while still being indexed by the graph and the structured-data surfaces.
---
title: 'Backlinks across the site'
permalink: '/backlinks/'
baselineExcludeFromGraph: true
---
The semantic boundary
Extraction is scoped semantically, not to the whole <body>:
- Single
<article>inside<main>→ that article is the boundary. - Multiple
<article>s (listing pages) → fall back to<main>. - No
<main>→ fall back to<body>. Defensive only.
Nav, header, footer, language switcher, chapter TOC, prev/next links: none of these end up in the graph. Pages also pass an outputPath.endsWith('.html') filter, so sitemap.xml, robots.txt, and JSON feeds drop out before parsing.
Accessors (modules)
createAccessors(getGraph) is the read interface modules use through the runtime layer. Closes over a getter so the underlying graph reference can swap on rebuilds.
| Accessor | Returns |
|---|---|
isReady() |
boolean |
getPage(url) |
node | undefined |
getHeadings(url) |
array |
getImages(url) |
array |
getExcerpt(url) |
string | undefined |
getBacklinks(url) |
Array<{ url, title?, excerpt? }> |
all() |
full nodes map |
getText and getOutgoingLinks are present in the accessor list but currently vestigial: text is no longer carried on nodes, and outgoing links moved into the flat edges array. Both flagged for either restoration or removal.
See also
- Page context - the per-page object the graph reads for identity.
- Globals - the cascade-key reference for
_node,_backlinks,_outgoing, and the_navigatorglobal. - Navigator module - registers the
_navigatorglobal. - Architecture snapshot - where the content graph sits in the three layers.