Custom schema with schema.pieces
Baseline assembles a complete JSON-LD graph for every page on its own: a WebSite, your Organization or Person identity, the WebPage, an Article where relevant, breadcrumbs, and the image and translation references, all wired together with stable @ids. That spine is automatic. When a page needs something the spine doesn't model, a Service, an FAQPage, a Product, an Event, it comes in through one seam: schema.pieces.
This page is for authors who already know schema.org and want to add nodes by hand. If you only want good defaults, you have them; you never have to open this.
The rule for the namespace is write
schema, readseo. You author identity and custom nodes under theschemacascade key; Baseline resolves everything into theseonamespace and the head module emits it. There is noseo.schema.
The seam: identity and pieces
data.schema, authored in _data/schema.js and on pages, has two reserved identity keys and one open extension key:
// _data/schema.js
export default {
organization: { '@type': 'Organization', name: 'a pleasant view', url: 'https://www.example.com/' }, // identity
person: { '@type': 'Person', name: 'Your Name' }, // identity
pieces: [] // extension seam: raw schema.org nodes, optional
};
organization and person are identity config. Baseline builds the Organization and Person spine nodes from them, stamps the @ids, and wires the cross-references (publisher, author). Because this is config you fill in, Baseline tidies it: null is the "not set" sentinel (logo: null), and null fields are dropped before the node is emitted.
pieces is a pass-through. Each entry is a raw schema.org node that Baseline feeds through the graph builder untouched, no validation, no coercion, no shape-fixing, and adds to the assembled graph.
The trust boundary: a piece is technical content written by someone who knows schema.org, so a malformed node is yours to catch in the Rich Results Test, not Baseline's to reject at build time. This is what every comparable tool does. Yoast's custom pieces are PHP a developer writes, Astro's is a graph you hand-assemble, RankMath emits correct shapes from a UI. Everyone trusts the supplier of the node to supply a valid one.
Scope by where you write it
schema.pieces is an ordinary cascade key, so you choose its scope by choosing where you write it. Eleventy concatenates arrays as it merges down the cascade, so pieces accumulate rather than replace:
- Per-page in front matter. Lands on that one page.
- Per-section in a directory data file. Lands on every page in the directory.
- Site-wide in
_data/schema.js. Lands on every page (the rare tail; most pieces are page- or section-scoped).
// content/services/services.11tydata.js – section scope
export default {
schema: {
pieces: [{ '@type': 'Service', name: 'Consulting' }]
}
};
A page under services/ then resolves to the site-wide pieces, plus the section's, plus its own, in cascade order. Concatenation is the default and the right one: the data file is the floor, narrower scopes add on top, the same way organization and person are already site-wide. The per-scope placement that other tools build custom UI for, Eleventy gives you from where the file sits.
For the rare case of dropping an inherited set on a single page, Eleventy's override: prefix replaces instead of concatenating:
---
schema:
override:pieces:
- { '@type': 'WebPage', 'name': 'Just this one' }
---
Baseline doesn't invent replace semantics or a removal API. The cascade already expresses scope.
Author pieces at the data layer: front matter, a directory data file, or
_data/schema.js. Schema placed undereleventyComputedis silently ignored. Baseline assembles the graph before page-level computed data resolves, so a computedschema.piecesnever reaches it. Plain data resolves in time; computed does not. This is also why projecting a collection into pieces needs more than a computed key, and why that how-to is still pending.
Writing inline nodes
Pages are YAML, and Baseline keeps it that way, no mixing ---json or ---js on rendered content. But schema.org nodes arrive as JSON, from schema.org's own examples, from Google's docs, and rewriting each one as block YAML is friction. The answer is inline JSON inside the YAML block:
---
title: Frequently asked
schema:
pieces:
- {
'@type': 'Question',
'name': 'Does Baseline lock me in?',
'acceptedAnswer': { '@type': 'Answer', 'text': 'No.' }
}
---
This isn't a second format. YAML 1.2 is a strict superset of JSON, so inline { } and [ ] are just YAML flow syntax, parsed by the same single YAML pass. The page stays entirely YAML; only the values are written in flow style. You can paste a schema.org node straight in and it works. Your formatter may tidy the double quotes to single ones, which is purely cosmetic, YAML accepts both.
The one rule that matters is quote your keys and your string values. Two reasons:
- It dodges the
@-key trap.@is a reserved indicator in YAML, so bare@type: Thingerrors while"@type"or'@type'is fine. Quote the key and the trap never appears, which is why pasted JSON, already quoted, just works. - It avoids YAML's scalar coercion. Unquoted,
name: yesbecomes a boolean andname: onlikewise; quoted values are immune.
Strip the quotes and you re-enter YAML's quirks. Baseline doesn't guard against that (see On validation below); the convention does the work a guard would.
Repeated entities
For a single custom node, an inline piece is enough. When you have many of one kind, an FAQ, a product catalogue, an event roster, keeping them all as inline pieces on one page gets unwieldy. The better shape is a directory where each entity is its own small file, gathered into a collection, so every entry stays individually editable.
The collection is ordinary site wiring: Baseline never learns the word "FAQ", for example, it only ever deals in schema nodes. Connecting that collection to the graph is a task in its own right and will get its own how-to. That guide isn't written yet.
On validation
Baseline does not schema-validate schema.pieces or any author-supplied content, and that is deliberate. The line runs along config versus content:
- Config (settings, options) gets structural validation at the plugin seams, enforcing only what would break the plugin.
- Content (front matter,
schema.pieces, authored nodes) gets none. It is trusted and passed through; a malformed value is yours to catch at runtime or in the Rich Results Test.
Re-checking a piece against an expected schema would re-implement work you already own, for self-inflicted errors, and contradict the trust the seam is built on. The convention, paste nodes as JSON and keep the quotes, does the work a validator would at zero runtime cost.
This is a current-state stance, not a permanent law. As Baseline grows, opt-in structural validation may earn its place through Eleventy's per-template eleventyDataSchema hook. When it does, it stays structural and opt-in, validating what would break and passing the rest through, not blanket content policing.
See also
- Site settings for the
settings.seodefaults and the SEO front-matter keys. - head module for how the assembled graph reaches the page.