All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
peerDependencies.emdashbumped to^0.6.0. Picks up eager taxonomy-term hydration ongetEmDashEntryresults (emdash PR #626), plus the toolbar portableText fix (#685) and several cold-start / D1 replica performance improvements.
- Taxonomy keywords and section in Article schema. For content pages the plugin now calls
getEmDashEntryto read the entry's eagerly-hydrateddata.termsmap (zero extra DB queries when the Astro template already fetched the entry in the same request). All assigned taxonomy terms are joined into thekeywordsfield of theBlogPostingnode. Terms from any taxonomy whose name starts withcategorare additionally mapped toarticleSection.
peerDependencies.emdashbumped to^0.5.0. This release depends on emdash-cms/emdash#536, #539, and #540, all shipped inemdash@0.5.0. Older EmDash versions no longer satisfy the peer range.
llms.txtand schema map now actually enumerate content. Both loops read the top-levelContentItem.slug/status/localefields exposed by #536 + #539, and narrow at the SQL layer viawhere: { status: "published" }from #540 instead of filtering in userland. Before this change, both routes silently returned empty against real EmDash sites — documented as broken in the v0.9.0 README warning.- Fuzzy Redirects candidate list is no longer empty. The tool fetches published URLs from
schema/map, so that route working fixes the candidate side of the feature for free. The other half (the 404 log capturing/404instead of the original requested URL) is still tracked in emdash-cms/emdash#525.
- Transient type casts. The
@ts-expect-errorinmetadata.ts(forrel: "nlweb") and the per-call type casts inllms.ts/schema/endpoints.tsare gone — the types ship withemdash@0.5.0. - "Known broken" README warning — the content-enumeration features now work.
- NLWeb
<link>tag. When the NLWeb endpoint URL setting is populated, the plugin contributes<link rel="nlweb" href="…">on every rendered page so conversational agents can discover the site's chat surface. Requires the EmDash release that contains emdash-cms/emdash#523 (merged); on older versions the sandbox allowlist rejects therelvalue and the contribution is silently dropped. The contribution site carries a transient@ts-expect-erroragainst the currently-published emdash types — it auto-unblocks once the new emdash version is on npm.
- Three earlier-shipped features (
llms.txt,schema/map,Fuzzy Redirects) silently produce empty results against real EmDash sites. Root causes are upstream:ContentItemstripsslug/status/locale(emdash-cms/emdash#530), and the 404 logging middleware captures the rewritten/404instead of the original requested URL (emdash-cms/emdash#525). README now carries a prominent warning at the top with the table of affected features and the upstream tracking links. No code change — the features will start working once upstream lands the fixes.
- Fuzzy Redirects admin tool. New admin page that reads the core 404 log, pairs each missing path with the closest published URLs (from the plugin's own
schema/maproute), and lets you one-click create a 301 redirect for the chosen destination. Scoring combines Levenshtein distance, token overlap, and a tokenized last-segment match bonus — so moved slugs, single-character typos, and punctuation drift all surface good suggestions. A minimum-score slider tunes how aggressive the suggestions are. rankCandidates()/scoreSlugMatch()exported fromsrc/fuzzy.tsfor reuse. These will plug straight into the eventualnotfoundhook (emdash-cms/emdash#525) once upstream lands — same matching logic, automatic trigger.
- Schema map (experimental). New public plugin route
schema/mapreturning the list of every published URL backed by schema markup ({ items: [{ url, collection, updatedAt }] }). Wire it to/schemamap.xmlat your site root with the Astro snippet in the README so agents and crawlers can enumerate structured-data URLs without scraping HTML. Per-URL schema endpoints (/schema/<slug>.json) are deferred pending an upstream helper for building page contexts from plugin routes.
- llms.txt (experimental). Generates a small-form
llms.txtindex of all published content across every collection that has aurlPattern, grouped by collection label. Exposed on the plugin routellms/txt; serve it from Astro by proxying that route at/llms.txt. Enabled by default — flip the llms.txt (experimental) toggle to disable. Only the plainllms.txtis implemented; thellms-full.txtvariant is out of scope. buildLlmsTxt()exported for consumers who want to assemble the body fromgetEmDashCollection()results with custom sectioning or ordering.
- Bump
@jdevalk/seo-graph-corerange from^0.2.0to^0.3.0and@jdevalk/astro-seo-graphrange from^0.2.1to^0.2.4. This dedupesseo-graph-corein consumernode_modules—astro-seo-graph@0.2.4pinsseo-graph-core@0.3.0as a direct dep, and the plugin's previous^0.2.0range excluded 0.3.0 (pre-1.0 semver), so consumers ended up with two parallel copies ofseo-graph-core. They now resolve to a single copy. seo-graph-core@0.3.0ships three additive improvements this plugin doesn't currently exercise directly: no moreinLanguage: 'en-US'default on piece builders, optionalWebPageInput.breadcrumb, and a generic type parameter onbuildOrganization. No behavioural change in this plugin's output.
- Bump
@jdevalk/seo-graph-corerange from^0.1.0to^0.2.0to pick up the new core release.seo-graph-core@0.2.0adds a generic type parameter tobuildOrganization(flows schema-dts subtype autocomplete into theextrafield) and makesWebPageInput.breadcrumboptional. Both improvements are additive; this plugin doesn't currently call those piece builders directly, but the bump keeps the installedseo-graph-coreversion in sync with what@jdevalk/astro-seo-graph@^0.2.1pulls in, avoiding duplicateseo-graph-corecopies innode_modules.
- hreflang alternates for multilingual sites. When EmDash's Astro
i18nconfig defines multiple locales and content entries are linked viatranslation_group, the plugin now emits one<link rel="alternate" hreflang="…" href="…">per published sibling plus an automatically-resolvedx-defaultentry. Self-referential entries are included. URLs are built from each collection'surlPattern+ the locale's Astro prefix rules, so every hreflang target matches the canonical URL of that page. - Region-tag hreflang output. BCP 47 tags are normalized on emission:
fr-cabecomesfr-CA,zh-hant-hkbecomeszh-Hant-HK. Sites that needfr-CAvsfr-FRas separate translations should use the code as the locale path (locales: ["en", "fr-ca", "fr-fr"]inastro.config.mjs), since EmDash core currently drops Astro's object-form{ path, codes }shape. buildPageUrlhelper (src/urls.ts). Shared path-construction logic honoringurlPattern,prefixDefaultLocale, and the plugin's canonical-normalization rules (lowercase, collapsed slashes, trailing slash).
- Runtime dependency on
@jdevalk/astro-seo-graph. The hreflang work reusesbuildAlternateLinksfromastro-seo-graphfor normalization, dedup, andx-defaultresolution. The helper is pure TypeScript, so this does not add any Astro runtime overhead — Astro is a peer dep satisfied transitively through EmDash.
Zero cost on single-locale sites: gated on isI18nEnabled() before any database call. When i18n is disabled, the hreflang path is a single boolean check and an early return.
- BreadcrumbList schema. Every non-homepage, non-404 page now emits a
BreadcrumbListentity in the schema graph, with a matchingbreadcrumb: { "@id": ... }back-reference on theWebPagenode. Two override layers:- Segment labels — a settings-level
segment → display labelmap (editable in the admin UI under Breadcrumbs → Segment labels). Overrides the default title-cased segment name wherever that segment appears in a path. Example:blog → Blogrelabels/blog/on every post under it. - Page type rules — a settings-level
pageType → ordered crumb listmap (editable in the admin UI under Breadcrumbs → Page type rules). Advanced, JSON-edited. Each crumb is{ label, href? }wherelabelmay contain the{title}placeholder and an omittedhrefresolves to the current canonical URL.
- Segment labels — a settings-level
- Default path derivation — when no rule matches, breadcrumbs are derived from
page.pathby walking segments, applying the label map or title-casing dashes, skipping numeric year/month segments (/2025/) andpage/Npagination, and always usingpage.titlefor the final crumb.
- Breaking:
@idscheme migrated tomakeIds()from@jdevalk/seo-graph-core. All entity@idvalues now match joost.blog's scheme for consistency across the SEO graph ecosystem. Notable shifts:WebPage@idis now the canonical URL itself (previously${url}#webpage).WebSite@idis${site}/#/schema.org/WebSite(previously${site}/#website).Person@idis${personUrl}#/schema.org/Person(previously a name-hashed path). Set thepersonUrlfield to relocate to e.g./about-me/.Organization@idis${site}/#/schema.org/Organization/${slug}whereslugis derived from the configured org name.isPartOf/mainEntityOfPagereferences onArticleupdated to point at the newWebPage@id.
buildAuthorPersonandbuildSiteEntitynow shareids.personfor Person sites, collapsing what were two near-identical nodes into one viaassembleGraph's first-wins dedupe.- The schema orchestrator constructs a single
IdFactoryonce per page and threads it through every piece builder, making@idgeneration testable and consistent.
This is a breaking change in the emitted JSON-LD @id values. Consumers that depend on specific @id strings (custom schema consumers, analytics that index by @id) will need to update. The overall graph shape is unchanged — only the identifier strings shift.
- Runtime dependency on
@jdevalk/seo-graph-core— the shared schema.org graph infrastructure also used by joost.blog via@jdevalk/astro-seo-graph. Both consumers now emit structurally-identical graphs even though they build pieces from very different runtime contexts (EmDash'sPublicPageContexthere, Astro's content collections there).
src/schema/index.tsnow usesassembleGraphfrom@jdevalk/seo-graph-coreto wrap the graph, rather than manually constructing the{ @context, @graph }envelope. This means both consumers share the same first-wins deduplication semantics out of the box. The individual piece builders (buildSiteEntity,buildWebSite,buildWebPage,buildArticle,buildAuthorPerson) remain EmDash-specific for now because EmDash's@idscheme, Organization-or-Person site entity dispatch, and WebSite SearchAction shape don't map cleanly onto the core's opinionated piece builders.- Piece builder return types aligned with core's
GraphEntitytype for type consistency across the ecosystem.
This release is a minor bump rather than a patch because introducing a runtime dependency is a material change in the plugin's install footprint, even though the JSON-LD output for existing pages is byte-identical to 0.1.3.
0.1.0 - 2026-04-03
- Meta description generation with configurable fallback chain
- Meta robots directives with
max-snippet,max-image-preview, andmax-video-preview;noindexfor search/utility pages - Canonical URL generation — absolute, normalized, with trailing slash and pagination support
- Open Graph tags —
og:title,og:type,og:image,og:url,og:description,og:site_name,og:locale - Twitter Card tags —
summary_large_imagewith site handle - JSON-LD schema graph with
Person/Organization,WebSitewithSearchAction,WebPage/CollectionPage, andArticlewith author - Admin settings UI auto-generated from
settingsSchema - Configurable site identity (Person or Organization), social profiles, title separator, and default description