Summary
Add an optional breadcrumbs field to PublicPageContext so themes can publish a breadcrumb trail as part of the page context, and SEO plugins (and any other page:metadata consumer) can read it without having to invent their own per-theme override mechanism.
Motivation
Breadcrumb trails are inherently theme-specific — the information architecture lives in the theme's routing, taxonomy decisions, and collection hierarchy. A generic SEO plugin has no way to know that /blog/2025/my-post/ should be Home > Blog > My Post rather than Home > Blog > 2025 > My Post, or that /team/joost/ should be Home > Team > Joost.
@jdevalk/emdash-plugin-seo v0.3.0 ships with two override layers (a segment-label map and a per-pageType JSON rule engine), both settings-driven. That covers the common cases but can't match a theme that builds breadcrumbs from its content hierarchy (parent pages, taxonomy walks, custom page types).
The natural place to express this is the theme's own context-building code — the theme already knows the parent page, the taxonomy, and the page type. What's missing is a place on PublicPageContext to put the result.
Why not a config callback on the plugin?
I considered this first. It doesn't work through the current plugin loader: generatePluginsModule at `packages/core/src/astro/integration/virtual-modules.ts:200` serializes `descriptor.options` with `JSON.stringify`, so functions are stripped before they reach the runtime plugin. Any theme-supplied callback would be silently dropped.
Pushing the data onto the page context is cleaner anyway — it's observable, debuggable, and available to any `page:metadata` consumer rather than being coupled to one plugin's config shape.
Proposed shape
```ts
// packages/core/src/plugins/types.ts
export interface BreadcrumbItem {
/** Display name for this crumb. /
name: string;
/* Absolute or root-relative URL for this crumb. */
url: string;
}
export interface PublicPageContext {
// ... existing fields ...
/**
- Optional breadcrumb trail for this page, root first. When set,
- SEO plugins should use this verbatim rather than deriving a
- trail from `path`. Themes typically populate this at the point
- they build the context (e.g. from a content hierarchy walk).
- Omit entirely (or set to `undefined`) if the theme has no opinion
- — consumers will fall back to their own defaults.
*/
breadcrumbs?: BreadcrumbItem[];
}
```
Also export `BreadcrumbItem` from the `emdash` package root so plugins can import the type.
Consumer contract
When `breadcrumbs` is present on the context, SEO plugins should:
- Use it as-is for `BreadcrumbList` schema emission
- Skip their own derivation/rule layers entirely for that page
- Treat `breadcrumbs: []` as "this page has no breadcrumbs" (e.g. homepage, error pages) and skip emission
When `breadcrumbs` is `undefined`, plugins fall back to whatever they do today (path derivation, rule maps, etc).
Migration
Non-breaking:
- Optional field, default `undefined`
- Existing themes work unchanged
- Existing SEO plugins continue to work; they can adopt the new field incrementally
Reference
`@jdevalk/emdash-plugin-seo` v0.3.0 will consume this field as Layer 0 (highest priority) once it lands, above its existing `breadcrumbRules` / `breadcrumbLabels` / path-derivation fallback chain.
Summary
Add an optional
breadcrumbsfield toPublicPageContextso themes can publish a breadcrumb trail as part of the page context, and SEO plugins (and any otherpage:metadataconsumer) can read it without having to invent their own per-theme override mechanism.Motivation
Breadcrumb trails are inherently theme-specific — the information architecture lives in the theme's routing, taxonomy decisions, and collection hierarchy. A generic SEO plugin has no way to know that
/blog/2025/my-post/should beHome > Blog > My Postrather thanHome > Blog > 2025 > My Post, or that/team/joost/should beHome > Team > Joost.@jdevalk/emdash-plugin-seov0.3.0 ships with two override layers (a segment-label map and a per-pageTypeJSON rule engine), both settings-driven. That covers the common cases but can't match a theme that builds breadcrumbs from its content hierarchy (parent pages, taxonomy walks, custom page types).The natural place to express this is the theme's own context-building code — the theme already knows the parent page, the taxonomy, and the page type. What's missing is a place on
PublicPageContextto put the result.Why not a config callback on the plugin?
I considered this first. It doesn't work through the current plugin loader:
generatePluginsModuleat `packages/core/src/astro/integration/virtual-modules.ts:200` serializes `descriptor.options` with `JSON.stringify`, so functions are stripped before they reach the runtime plugin. Any theme-supplied callback would be silently dropped.Pushing the data onto the page context is cleaner anyway — it's observable, debuggable, and available to any `page:metadata` consumer rather than being coupled to one plugin's config shape.
Proposed shape
```ts
// packages/core/src/plugins/types.ts
export interface BreadcrumbItem {
/** Display name for this crumb. /
name: string;
/* Absolute or root-relative URL for this crumb. */
url: string;
}
export interface PublicPageContext {
// ... existing fields ...
/**
*/
breadcrumbs?: BreadcrumbItem[];
}
```
Also export `BreadcrumbItem` from the `emdash` package root so plugins can import the type.
Consumer contract
When `breadcrumbs` is present on the context, SEO plugins should:
When `breadcrumbs` is `undefined`, plugins fall back to whatever they do today (path derivation, rule maps, etc).
Migration
Non-breaking:
Reference
`@jdevalk/emdash-plugin-seo` v0.3.0 will consume this field as Layer 0 (highest priority) once it lands, above its existing `breadcrumbRules` / `breadcrumbLabels` / path-derivation fallback chain.