Releases: jdevalk/seo-graph
@jdevalk/seo-graph-core@0.6.2
Patch Changes
-
ff5adcb: Fix
validateIndexNowKeyto accept the full character set allowed by the IndexNow spec —[A-Za-z0-9-]— instead of only hexadecimal. Keys issued by Ahrefs Site Audit, Yandex Webmaster, and other tools that contain uppercase letters pastF, lowercase letters pastf, or dashes were previously rejected with"IndexNow key must be 8–128 hex characters.", forcing users to register a second key with the engines. Real-world impact: Bing returnedUserForbiddedToAccessSiteonce a hex key was registered alongside an existing non-hex key.Updated error messages and JSDoc to reflect the broader allow-list and renamed the internal
HEX_KEY_REconstant toKEY_RE.generateIndexNowKeycontinues to emit hex — fine default for fresh keys; the validator change just stops rejecting equally-valid non-hex keys from elsewhere.Fixes #35.
@jdevalk/astro-seo-graph@2.0.0
Major Changes
-
a32a1d4: Breaking: drop Astro 5 from peer deps; require Astro 6 and zod 4.
Why: Astro 5.x ships zod 3 and Astro 6.x ships zod 4. Supporting both Astro majors meant shipping zod 3 as a runtime dep while users on Astro 6 had zod 4 from
astro:content. zod brands schemas with version-specific symbols, soseoSchema(image)(returned as a zod 3 schema) couldn't compose cleanly into a user'sz.object({ ... })fromastro:content(zod 4) — composition produced TS type errors and worked at runtime only by accident.What changed:
peerDependencies.astrois now^6.0.0(was^5.0.0 || ^6.0.0).dependencies.zodis now^4.4.3(was^3.24.0).
What didn't change:
- The exported API surface (
<Seo>,createSchemaEndpoint,createSchemaMap,createApiCatalog,createMarkdownEndpoint,createIndexNowKeyRoute,seoSchema,imageSchema,buildAlternateLinks,breadcrumbsFromUrl,gitLastmod,aggregate,renderLlmsTxt,renderMarkdownAlternate, all types) — all unchanged. - Our zod usage is core surface only (
z.object,z.string,z.enum,.min,.max,.optional,.default) — no zod 4-specific syntax.
Migration:
If you're already on Astro 6, install:
pnpm add @jdevalk/astro-seo-graph@2. YourseoandfeatureImagecollection fields will start composing without the silent type drift they had under 1.x.If you're still on Astro 5, stay on
@jdevalk/astro-seo-graph@1.4.1. The 1.x line gets bug fixes only — no new features ship to it.
@jdevalk/astro-seo-graph@1.4.1
Patch Changes
-
20e8049: Fix
deriveMdUrlso the site root (/) maps to/index.mdinstead of/.md. The auto-emitted<link rel="alternate" type="text/markdown">on the homepage previously pointed athttps://example.com/.md, which doesn't match the file Astro produces fromsrc/pages/index.md.ts(/index.md). The build-end verification then stripped the link and warned per occurrence. The new behavior matches Astro's filesystem routing for the index route — homepages with a markdown alternate now keep the discovery link.Fixes #40.
-
Updated dependencies [ff5adcb]
- @jdevalk/seo-graph-core@0.6.2
@jdevalk/astro-seo-graph@1.4.0
Minor Changes
-
9c67905: Add
createApiCatalogroute factory: serves an RFC 9727 API catalog at/.well-known/api-catalogasapplication/linkset+json(RFC 9264). Accepts three categories of entry — schema.org JSON endpoints (auto-typed ashttps://schema.org/<schemaType>), the schema map (no type field, no standard type exists), and free-formadditionalAPIs. Relative paths are absolutized againstsiteUrl; absolute URLs pass through unchanged.Also exports a
CATALOG_PATHconstant ('/.well-known/api-catalog') so callers can reference the path from_headersfiles, the schemamap, or documentation links without duplicating the string.Pairs with the existing
createSchemaEndpointandcreateSchemaMapfactories: schemas are listed once in catalog config, the catalog auto-fillstypeURLs, and the wire format is fixed by RFC 9727 so there's no ambiguity for agent crawlers. -
f7e2e64: Add
gitLastmodhelper: reads the committer date of the most recent git commit that touched a file, with configurableexcludeCommits(skip bulk imports / reformats / renames) anddepth. Use it to feed trustworthydateModified/<lastmod>values from git history instead of filesystemmtime, which gets rewritten on every CI checkout.Returns
nullwhen the file has no git history, git isn't on the PATH, or every commit in the inspected window is excluded — callers should fall back topublishDate(or skip the field) in that case.excludeCommitsmatches on the first 7 characters of the SHA, so short hashes fromgit log --onelinework directly.Build-time only — shells out to the
gitbinary viaexecFileSync(no shell parsing, so file paths containing quotes or$are safe).
@jdevalk/astro-seo-graph@1.3.0
Minor Changes
-
b82bddb: Verify markdown-alternate links against the build output.
When
seoGraph({ markdownAlternate: true })is enabled, the integration now walks the build output after every build, finds each auto-emitted<link rel="alternate" type="text/markdown">, checks whether the referenced.mdfile is actually on disk, and strips any link whose target is missing — with awarnper occurrence so misconfigured endpoints stay visible.Mirrors the existence check in
astro-markdown-alternate. Previously<Seo>derived the markdown URL optimistically from the canonical and could ship a link pointing at a 404 (collection entries that didn't get prerendered, routes that don't cover this page).New exports for callers building their own pipelines:
findMarkdownAlternateLink,stripMarkdownAlternateLink,resolveMarkdownAlternatePath.SSR users whose
.mdendpoints aren't prerendered should leavemarkdownAlternateoff and emit the discovery link themselves — the on-disk verification will otherwise strip every link.
@jdevalk/astro-seo-graph@1.2.0
Minor Changes
-
6e48ee8: Add markdown-alternate support: serve clean markdown versions of pages at parallel
.mdURLs so AI agents (Claude, ChatGPT, Perplexity, Cloudflare's AI crawlers) can consume content without HTML parsing.createMarkdownEndpoint— factory returning an AstroAPIRoutethat serves a markdown entry: YAML frontmatter (title, canonical, pubDate, updatedDate, author, description, tags, categories) + body. Ships withContent-Type: text/markdown; charset=utf-8,X-Robots-Tag: noindex, follow,X-Markdown-Tokens: <n>, andLink: <canonical>; rel="canonical"pointing crawlers at the HTML. Token count defaults to a roughchars/4estimate; swap ingpt-tokenizeror@anthropic-ai/tokenizerviaestimateTokensfor accuracy.renderMarkdownAlternate— pure renderer behind the endpoint. Importable from non-Astro code for the same frontmatter + body + token-count output.Auto-emitted discovery link (opt-in) — new
markdownAlternate?: booleanoption on theseoGraph()integration. Whentrue,<Seo>emits<link rel="alternate" type="text/markdown" href="…">on every page withhrefderived from the canonical (e.g./blog/post/→/blog/post.md). Default isfalse— enable only after wiring upcreateMarkdownEndpointat the matching path, or the link will 404. Implemented via a Vitedefinethat replaces a sentinel in the compiled<Seo>component at build time — no runtime cost.Cloudflare content negotiation — README gains a recipe for honouring
Accept: text/markdownon static sites via CF Transform Rules (URL rewrite +Vary: Acceptresponse header), no SSR or middleware required.No breaking changes. Existing
<Seo>consumers render byte-identical output unless they opt intomarkdownAlternate: true.
@jdevalk/astro-seo-graph@1.1.1
Patch Changes
-
e62b8c3: Fix two bugs in the 1.1.0 validators.
validateInternalLinksflagged static assets as 404s. The
built-paths set was constructed only from*.htmlfiles, so any link
to a file copied frompublic/(images, fonts, downloads,
favicon.svg, etc.) came back asnot-found. The walker now collects
every file produced by the build and includes non-HTML paths verbatim
as link targets. HTML pages still map throughhtmlFileToPathso the
trailing-slash mismatch detection keeps working.New export:
buildLinkTargetSet(files)— turns a list of built files
into the valid-link-target set used byclassifyInternalLink.extractMetaDescriptiontruncated on apostrophes. The regex used
[^"']*to capture the attribute value, which terminates on either
quote — socontent="don't stop"was captured asdon, and
validateMetadataLengthreported absurdly short lengths like "11
chars" for descriptions that were actually 90. The extractor now uses
a backreference on the opening quote, so the capture only terminates
on the quote that opened it. Entity decoding is unchanged; the
returned value is the whitespace-collapsed, entity-decoded text that
Google renders in the SERP.
@jdevalk/astro-seo-graph@1.1.0
Minor Changes
-
e8367dc: Three new build-time validators on the
seoGraph()integration.validateImageAlt(defaulttrue) — warns about<img>tags
missing analtattribute. Common SEO and accessibility miss that's
easy to overlook while drafting. Respects both WCAG decorative-image
patterns:alt=""(the canonical form) and
role="presentation"/role="none"(removes the image from the
accessibility tree). Only a tag with neither is flagged. Warnings
identify each page and list the first fewsrcvalues so offenders
are findable without reopening the file.validateMetadataLength(defaulttrue) — warns when<title>or
<meta name="description">length falls outside SERP-friendly bounds.
Defaults: title 30–65 characters, description 70–200. Passfalseto
disable or an object to override per-field — e.g.
{ title: { max: 60 }, description: { min: 120 } }applies the
overrides and keeps defaults for the rest. Length is measured on the
whitespace-collapsed, entity-decoded text (the same thing Google
renders in the SERP).validateInternalLinks(defaulttrue) — warns when an internal
<a href>points to a URL that doesn't match a built page. Catches two
common bugs: trailing-slash mismatches (e.g. linking to/about-me
when the built page is/about-me/— "works" via redirect but wastes
a round-trip on every click) and true 404s. Only same-origin (via
config.site) and root-relative links are checked; external URLs,
mailto:/tel:, and fragment-only links are skipped. Explicit
redirects are honored as valid targets by default: literal sources in
public/_redirects(Netlify / Cloudflare Pages format) and literal
keys in Astro'sredirectsconfig are unioned into the built-paths
set. SethonorRedirects: falseto opt out when auditing for redirect
hops. Dynamic rules (wildcards, splats,[slug]params) are skipped;
useskipfor those. Pass{ skip: (href) => boolean }to exclude
additional hrefs (e.g. SSR-only routes).New exports on
@jdevalk/astro-seo-graph/integration:
findImagesWithoutAlt,resolveMetadataLengthBounds,
MetadataLengthBounds,DEFAULT_METADATA_LENGTH_BOUNDS,
classifyInternalLink,extractAnchorHrefs,htmlFileToPath,
parseNetlifyRedirects,collectAstroRedirectSources,
ValidateInternalLinksOptions— for callers running the same checks
in their own pipelines (pre-commit hooks, CI scripts, etc.).
@jdevalk/astro-seo-graph@1.0.1
Patch Changes
-
bf6efb2: IndexNow: exclude
/404(and/404/) from submission by default. Search
engines don't need to be notified about a site's 404 page, and submitting
it wastes daily IndexNow quota. The exclusion is applied before any
caller-suppliedfilterruns, so callers don't need to re-exclude it
themselves.Also documents the
filteroption alongside common patterns (e.g.
excluding paginated archives like/blog/2/), and exposes
isDefaultExcludedFromIndexNow(url)for callers building their own
submission pipelines.Closes #26.
@jdevalk/astro-seo-graph@1.0.0
Major Changes
-
fc161b9:
<Seo>is now a first-party component. Removes theastro-seodependency
and renders every<head>tag directly. Net result: cleaner output, two
silent bugs fixed, lighter install.Bug fixes (behavior changes that fix wrong output)
articlePublishernow actually renders. Previously the prop typechecked
but was silently dropped —astro-seo'sOpenGraphArticleTagsdoesn't
destructurepublisher, and itsopenGraph.articletype doesn't include
the field. Anyone usingarticlePublisherwas getting no output.<Seo>
now emits<meta property="article:publisher" content="…">as documented.
Filed upstream: jonasmerlin/astro-seo#110.<link rel="canonical">no longer leaks onnoindexpages. The
intent ("omit canonical on noindex" per Google recommendation) was
defeated byastro-seo's built-in canonical fallback that reconstructs
the tag fromAstro.urlwhen the prop is undefined.<Seo>now omits
the tag cleanly. Tracked upstream:
jonasmerlin/astro-seo#107.- Single
og:imagetag instead ofog:image+og:image:url. The
astro-seopath emitted both with the same value — synonymous, harmless,
but noise.<Seo>emits justog:image. - Single
<meta name="robots">tag. Theastro-seopath always
emitted its own defaultindex, followtag that couldn't be suppressed,
while we needed our own to addmax-snippet:-1, max-image-preview:large, max-video-preview:-1. Result was two robots tags per page.<Seo>now
emits one merged tag with all directives.
Cosmetic changes (HTML diff but equivalent meaning)
The byte-for-byte head output differs from 0.x in tag ordering. Search engines
treat these as identical, but consumers running snapshot tests against
<Seo>output will see diffs.- Tag grouping by concern: title → canonical → description → robots →
OG basic → OG optional → OG image meta → OG article → twitter →
hreflang → author → extras → JSON-LD. Previously the order was driven
byastro-seo's sub-components. - Twitter overrides follow the same field order as their OG counterparts
(card / site / creator / title / description / image / imageAlt). The
legacy order was card / site / title / image / imageAlt / description / creator. - Hreflang link attribute order:
rel→hreflang→href.
The legacy order wasrel→href→hreflang.
Breaking API changes
-
Removed
buildAstroSeoProps. Replaced bybuildSeoContext, which
returns a flat, render-ready normalization (SeoContext) rather than the
nestedastro-seo-shaped adapter. Migration:- import { buildAstroSeoProps } from '@jdevalk/astro-seo-graph'; - const props = buildAstroSeoProps(seo, Astro.url.href); - props.openGraph.basic.title // nested + import { buildSeoContext } from '@jdevalk/astro-seo-graph'; + const ctx = buildSeoContext(seo, Astro.url.href); + ctx.og.title // flat
-
Removed
AstroSeoPropstype export. The intermediate shape no
longer exists. UseSeoContext(the new normalized shape) orSeoProps
(the public input shape) depending on which side of the boundary you're on. -
Removed
astro-seofrom dependencies. If you imported it transitively
through this package, install it directly:pnpm add astro-seo.
New exports
buildSeoContext(props, url): SeoContext— pure-TS normalization.SeoContexttype — flat render-ready shape.ROBOTS_EXTRASconstant — themax-snippet:-1, max-image-preview:large, max-video-preview:-1directives<Seo>always appends to the robots tag.