Skip to content

Releases: jdevalk/seo-graph

@jdevalk/seo-graph-core@0.6.2

06 May 14:37
51bd514

Choose a tag to compare

Patch Changes

  • ff5adcb: Fix validateIndexNowKey to 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 past F, lowercase letters past f, 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 returned UserForbiddedToAccessSite once 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_RE constant to KEY_RE. generateIndexNowKey continues 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

06 May 18:06
2fb055b

Choose a tag to compare

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, so seoSchema(image) (returned as a zod 3 schema) couldn't compose cleanly into a user's z.object({ ... }) from astro:content (zod 4) — composition produced TS type errors and worked at runtime only by accident.

    What changed:

    • peerDependencies.astro is now ^6.0.0 (was ^5.0.0 || ^6.0.0).
    • dependencies.zod is 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. Your seo and featureImage collection 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

06 May 14:37
51bd514

Choose a tag to compare

Patch Changes

  • 20e8049: Fix deriveMdUrl so the site root (/) maps to /index.md instead of /.md. The auto-emitted <link rel="alternate" type="text/markdown"> on the homepage previously pointed at https://example.com/.md, which doesn't match the file Astro produces from src/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

04 May 10:29
55a5b32

Choose a tag to compare

Minor Changes

  • 9c67905: Add createApiCatalog route factory: serves an RFC 9727 API catalog at /.well-known/api-catalog as application/linkset+json (RFC 9264). Accepts three categories of entry — schema.org JSON endpoints (auto-typed as https://schema.org/<schemaType>), the schema map (no type field, no standard type exists), and free-form additional APIs. Relative paths are absolutized against siteUrl; absolute URLs pass through unchanged.

    Also exports a CATALOG_PATH constant ('/.well-known/api-catalog') so callers can reference the path from _headers files, the schemamap, or documentation links without duplicating the string.

    Pairs with the existing createSchemaEndpoint and createSchemaMap factories: schemas are listed once in catalog config, the catalog auto-fills type URLs, and the wire format is fixed by RFC 9727 so there's no ambiguity for agent crawlers.

  • f7e2e64: Add gitLastmod helper: reads the committer date of the most recent git commit that touched a file, with configurable excludeCommits (skip bulk imports / reformats / renames) and depth. Use it to feed trustworthy dateModified / <lastmod> values from git history instead of filesystem mtime, which gets rewritten on every CI checkout.

    Returns null when 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 to publishDate (or skip the field) in that case. excludeCommits matches on the first 7 characters of the SHA, so short hashes from git log --oneline work directly.

    Build-time only — shells out to the git binary via execFileSync (no shell parsing, so file paths containing quotes or $ are safe).

@jdevalk/astro-seo-graph@1.3.0

03 May 12:00
e97a51e

Choose a tag to compare

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 .md file is actually on disk, and strips any link whose target is missing — with a warn per 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 .md endpoints aren't prerendered should leave markdownAlternate off and emit the discovery link themselves — the on-disk verification will otherwise strip every link.

@jdevalk/astro-seo-graph@1.2.0

14 Apr 20:46
40badcd

Choose a tag to compare

Minor Changes

  • 6e48ee8: Add markdown-alternate support: serve clean markdown versions of pages at parallel .md URLs so AI agents (Claude, ChatGPT, Perplexity, Cloudflare's AI crawlers) can consume content without HTML parsing.

    createMarkdownEndpoint — factory returning an Astro APIRoute that serves a markdown entry: YAML frontmatter (title, canonical, pubDate, updatedDate, author, description, tags, categories) + body. Ships with Content-Type: text/markdown; charset=utf-8, X-Robots-Tag: noindex, follow, X-Markdown-Tokens: <n>, and Link: <canonical>; rel="canonical" pointing crawlers at the HTML. Token count defaults to a rough chars/4 estimate; swap in gpt-tokenizer or @anthropic-ai/tokenizer via estimateTokens for 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?: boolean option on the seoGraph() integration. When true, <Seo> emits <link rel="alternate" type="text/markdown" href="…"> on every page with href derived from the canonical (e.g. /blog/post//blog/post.md). Default is false — enable only after wiring up createMarkdownEndpoint at the matching path, or the link will 404. Implemented via a Vite define that 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/markdown on static sites via CF Transform Rules (URL rewrite + Vary: Accept response header), no SSR or middleware required.

    No breaking changes. Existing <Seo> consumers render byte-identical output unless they opt into markdownAlternate: true.

@jdevalk/astro-seo-graph@1.1.1

14 Apr 07:55
602ada9

Choose a tag to compare

Patch Changes

  • e62b8c3: Fix two bugs in the 1.1.0 validators.

    validateInternalLinks flagged static assets as 404s. The
    built-paths set was constructed only from *.html files, so any link
    to a file copied from public/ (images, fonts, downloads,
    favicon.svg, etc.) came back as not-found. The walker now collects
    every file produced by the build and includes non-HTML paths verbatim
    as link targets. HTML pages still map through htmlFileToPath so the
    trailing-slash mismatch detection keeps working.

    New export: buildLinkTargetSet(files) — turns a list of built files
    into the valid-link-target set used by classifyInternalLink.

    extractMetaDescription truncated on apostrophes. The regex used
    [^"']* to capture the attribute value, which terminates on either
    quote — so content="don't stop" was captured as don, and
    validateMetadataLength reported 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

14 Apr 07:25
7585dd4

Choose a tag to compare

Minor Changes

  • e8367dc: Three new build-time validators on the seoGraph() integration.

    validateImageAlt (default true) — warns about <img> tags
    missing an alt attribute. 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 few src values so offenders
    are findable without reopening the file.

    validateMetadataLength (default true) — warns when <title> or
    <meta name="description"> length falls outside SERP-friendly bounds.
    Defaults: title 30–65 characters, description 70–200. Pass false to
    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 (default true) — 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's redirects config are unioned into the built-paths
    set. Set honorRedirects: false to opt out when auditing for redirect
    hops. Dynamic rules (wildcards, splats, [slug] params) are skipped;
    use skip for 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

13 Apr 16:37
1d6c517

Choose a tag to compare

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-supplied filter runs, so callers don't need to re-exclude it
    themselves.

    Also documents the filter option 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

13 Apr 14:18
160054a

Choose a tag to compare

Major Changes

  • fc161b9: <Seo> is now a first-party component. Removes the astro-seo dependency
    and renders every <head> tag directly. Net result: cleaner output, two
    silent bugs fixed, lighter install.

    Bug fixes (behavior changes that fix wrong output)

    • articlePublisher now actually renders. Previously the prop typechecked
      but was silently dropped — astro-seo's OpenGraphArticleTags doesn't
      destructure publisher, and its openGraph.article type doesn't include
      the field. Anyone using articlePublisher was 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 on noindex pages. The
      intent ("omit canonical on noindex" per Google recommendation) was
      defeated by astro-seo's built-in canonical fallback that reconstructs
      the tag from Astro.url when the prop is undefined. <Seo> now omits
      the tag cleanly. Tracked upstream:
      jonasmerlin/astro-seo#107.
    • Single og:image tag instead of og:image + og:image:url. The
      astro-seo path emitted both with the same value — synonymous, harmless,
      but noise. <Seo> emits just og:image.
    • Single <meta name="robots"> tag. The astro-seo path always
      emitted its own default index, follow tag that couldn't be suppressed,
      while we needed our own to add max-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
      by astro-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: relhreflanghref.
      The legacy order was relhrefhreflang.

    Breaking API changes

    • Removed buildAstroSeoProps. Replaced by buildSeoContext, which
      returns a flat, render-ready normalization (SeoContext) rather than the
      nested astro-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 AstroSeoProps type export. The intermediate shape no
      longer exists. Use SeoContext (the new normalized shape) or SeoProps
      (the public input shape) depending on which side of the boundary you're on.

    • Removed astro-seo from 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.
    • SeoContext type — flat render-ready shape.
    • ROBOTS_EXTRAS constant — the max-snippet:-1, max-image-preview:large, max-video-preview:-1 directives <Seo> always appends to the robots tag.