---
title: 'SEO graph'
description: 'The resolved per-page SEO object baseline builds at cascade-time and the head module emits at transform-time: canonical, Open Graph, Twitter, and a JSON-LD graph.'
slug: 'seo-graph'
type: 'article'
date: 2026-06-07T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/core-reference/seo-graph/'
---

A resolved per-page object: the canonical URL, the Open Graph and Twitter projections, and a JSON-LD `@graph`. Built once during the data cascade, cached, and read again at transform-time when the head module emits the tags.

You access it as `_seoGraph` in templates. It is the per-page sibling of [[page-context | page context]] — same lifecycle, different payload. Where page context carries the normalised values for `<title>` and `<head>` links, the SEO graph carries the structured-data and social surface: the JSON-LD nodes, the share-card metadata, the canonical.

---

## Why it exists

Eleventy's HTML transformer hook, where the head module emits its tags, cannot see the data cascade. The SEO graph is built during the cascade and cached in a registry keyed by `page.url`, so the transform-time driver can read the resolved object back after the cascade has closed.

It is the same trick page context uses, for the same reason: build once on the cascade, read once at transform-time, never re-derive.

---

## Where it surfaces

- **In templates**: as `_seoGraph` (computed via `eleventyComputed`).
- **In `_snapshot`**: every built graph lives under `_snapshot.seoGraph`, keyed by URL. No underscore — it is the inspection snapshot surface. See [[globals | Globals]].
- **At transform-time**: read by the head module via the registry's `getByKey(page.url)` lookup.

---

## What you write

You do not write `_seoGraph`. You write two input keys and the plugin derives the handle from them, plus the site defaults.

| Key            | Where                            | Carries                                                                                                                              |
| -------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `settings.seo` | site config                      | SEO site defaults: `preserveQueryParams`, `ogImage`, `openGraph`, `twitter`                                                          |
| `seo:`         | page front matter                | per-page _presentation_ overrides: `title`, `description`, `canonical`, the OG and Twitter fields. `seo.foo` wins over a bare `foo`. |
| `schema:`      | `_data/schema.js` + front matter | schema.org _identity and extension_: `organization`, `person`, `pieces`                                                              |

The split: `seo:` is how the page presents (the share card, the canonical); `schema:` is what the page _is_ (who published it, plus any extra nodes). The full key lists live in [[site-settings | Site settings]] (`settings.seo` and the `seo:` keys) and [[custom-schema | Custom schema]] (the `schema:` surface).

One naming point worth holding: you write `schema`, you read the built form at `_seoGraph.schema`. Same name, different shapes — input `schema` is `{ organization, person, pieces }`; output `_seoGraph.schema` is the flat, resolved array of schema.org nodes. The name is shared because one is the built form of the other, not because they match.

---

## The SEO graph shape

Four keys.

```js
_seoGraph = {
	url, // resolved canonical URL; undefined when noindex or no resolvable input
	schema, // resolved JSON-LD @graph: flat array of schema.org nodes
	openGraph, // Open Graph projection: type, title, description, url, image, locale, article fields
	twitter // Twitter card projection: card, site, creator, title, description, image
};
```

`schema` is the assembled `@graph`: a `WebSite` node, an `Organization` or `Person` identity, the page's `WebPage` (and an `Article` where the type calls for one), a `BreadcrumbList`, image and translation refs, and any nodes you supplied through `schema.pieces`. The emitted JSON-LD wraps this array as `@graph`.

Fields you set on `seo:` that the resolver does not overwrite pass through onto the object unchanged.

---

## Reading it in a template

Most pages never touch `_seoGraph` — the head module reads it and emits the canonical, the OG and Twitter metas, and the JSON-LD for you. Reach for it when you want to inspect the resolved values:

{% raw %}

```nunjucks
{# The resolved canonical, the same value the head module emits #}
<link rel="canonical" href="{{ _seoGraph.url }}">

{# The Open Graph type the page resolved to #}
<meta property="og:type" content="{{ _seoGraph.openGraph.type }}">
```

{% endraw %}

`_seoGraph` is `null` on pages that opt out (see below), so guard the access if your template runs on those.

---

### Enriching the graph at render time

Because `_seoGraph` is on the cascade, a render-time slot can extend it before the head emits. A function the template calls receives the resolved object on `this.ctx._seoGraph`, and a mutation of `this.ctx._seoGraph.schema` rides through to the head driver — it is the same cached object, read back at transform-time.

The docs site's FAQ page does this: a front-matter function, called from the body, patches the resolved `FAQPage` node with questions built from a collection. The head then emits the enriched JSON-LD. It is an advanced seam — most pages set `schema:` in front matter and never reach for it — but it is there when a node needs values that only exist after the cascade resolves.

---

## Opt out: `_internal: true`

Templates that set `_internal: true`, or that have a non-HTML output extension, are skipped — `_seoGraph` resolves to `null`. Sitemaps, feeds, schema endpoints, and the markdown-sibling renderer all opt out this way: they are not pages that need a share card or a JSON-LD graph.

---

## See also

- [[site-settings | Site settings]] - `settings.seo` defaults and the `seo:` front-matter keys.
- [[custom-schema | Custom schema]] - writing `schema:` identity and `pieces`.
- [[page-context | Page context]] - the sibling per-page substrate, same lifecycle.
- [[head | Head module]] - the consumer; reads `_seoGraph` at transform-time and emits the tags.
- [[globals | Globals]] - `_snapshot.seoGraph`, the inspection snapshot.
- [[internals | Internals]] - the registry primitive that backs the cache.
