Eleventy Baseline

Start building your site, skip the recurring setup work.
Table of Contents

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 in INTERNAL_KEYS but currently unbound.
  • Nunjucks global: _navigator carries { 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 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.


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: true is 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: true is 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>:

  1. Single <article> inside <main> → that article is the boundary.
  2. Multiple <article>s (listing pages) → fall back to <main>.
  3. 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 _navigator global.
  • Navigator module - registers the _navigator global.
  • Architecture snapshot - where the content graph sits in the three layers.

Previous: Page context

Next: Internals