Skip to content

Add breadcrumbs?: BreadcrumbItem[] to PublicPageContext for theme-driven breadcrumb trails #413

@jdevalk

Description

@jdevalk

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:

  1. Use it as-is for `BreadcrumbList` schema emission
  2. Skip their own derivation/rule layers entirely for that page
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions