Repository navigation
Releases
v1.22.1 — afdocs 0.23.0
Bumps the pinned afdocs scanner CLI from 0.22.2 to 0.23.0. Re-verified end to end (verify-loop on all fixtures, lint, live scan identical to 0.22.2), so no adapter changes were needed. No behavior change otherwise. Fixture-only dependency bump (undici) is not part of the published package.
v1.22.0 — .md-sibling duplicate check + fixer
New md-duplicate-signal check in the agent-headers scanner: fails when a .md mirror and its HTML twin both return 200 and the .md has neither a canonical Link header nor X-Robots-Tag: noindex. This is the pattern behind Search Console's "duplicate without user-selected canonical" alerts on sites that publish markdown siblings for agents.
enhance fixes it per platform: Cloudflare Pages and Netlify merge a /*.md noindex rule into _headers, Vercel merges one into vercel.json, CloudFront/OpenNext gets a warning with the exact response-headers-policy instruction, and GitHub/GitLab Pages stay report-only. Existing rules are never rewritten.
Also fixes agent-headers missing .md links in Starlight-format llms.txt files (bare root-relative paths), which made the new check skip on exactly the sites it targets.
v1.21.0 — SvelteKit + Cloudflare Pages fixer
New fixer for SvelteKit (adapter-static) on Cloudflare Pages, including monorepos. enhance wires a zero-dependency postbuild script that edits only the build output: missing canonical link, homepage JSON-LD (WebSite, override with AGENT_READINESS_SCHEMA_TYPE), a hidden llms.txt pointer, .md mirrors, /404.md and llms.txt, each only when absent. Adds functions/_middleware.js for Accept: text/markdown negotiation at the Pages project root. Site source is never touched and existing files are never overwritten.
Local result on a real SvelteKit docs site: afdocs 85 → 95.
Also includes the 1.20.0 engines floor (Node >=22) that was not previously published to npm.
v1.20.0
Engines floor raised: engines.node is now >=22 (was >=18). Node 20 and older are end of life; Node 22 stays supported. CI runs on Node 24 with current GitHub Actions. No CLI behavior change.
Not published to npm yet; npm latest is still 1.19.3.
v1.17.0
Fixed: Starlight .md mirrors now stand on their own
The src/pages/[...slug].md.ts route that enhance writes for Astro + Starlight served each page's
raw MDX body. Two problems, both found on a real production Starlight docs site:
- Relative links broke.
../guide/in a page at/docs/page/resolves to a different place
next to/docs/page.md, and to nothing once an agent has the text without its URL. afdocs 0.22's
markdown-link-portabilitycheck fails on this. - Component text was hidden. Starlight components stayed as raw JSX, and a
LinkCard's title
and description live in attributes, so card-based landing pages lost most of their text in the
markdown (36–90% missing on that site). afdocs'markdown-content-paritycheck fails on this.
The mirror now runs the body through toAgentMarkdown() in a new src/lib/md-mirror.js:
- Every link and image target is resolved against the page's HTML URL: absolute when
astro.configsetssite, root-relative otherwise.baseis respected. LinkCardbecomes a list item with the description on its own line under the link,
LinkButtona link,Badge(text),Asidea**Note:**/title label,Card/TabItema bold
label.CardGrid,Steps,Tabs,FileTreewrappers and import lines are dropped.- Fenced code is left untouched.
If you already ran enhance: it never overwrites your existing [...slug].md.ts. Re-running
siteready enhance now writes src/lib/md-mirror.js and warns when your route still serves the raw
body. Wire it in:
import { toAgentMarkdown } from '../lib/md-mirror.js';
// ...
const root = import.meta.env.BASE_URL.replace(/\/?$/, '/');
const slug = params.slug === 'index' ? '' : params.slug;
const pageUrl = new URL(`${root}${slug ? `${slug}/` : ''}`, site ?? 'http://localhost/');
const body = toAgentMarkdown(entry.body ?? '', pageUrl, Boolean(site));(params and site come from the GET handler's context.)
v1.16.1
Changed: afdocs 0.20.0 → 0.22.2
The afdocs scanner now runs at 0.22.2. Its JSON output is compatible with siteready's scorecard,
so reports keep the same shape. afdocs 0.22 adds five checks:
bot-protection-interferenceembedded-data-serializationmarkdown-link-portabilitypage-size-transfersingle-fetch-completeness
Scores can move because of the new checks and weights, not because your site changed. A
diff-report between a baseline scanned with 1.16.0 or earlier and a rescan with 1.16.1 lists
these five as new checks. For a clean before/after comparison, re-run the baseline with 1.16.1.
To try a different afdocs version for one run without upgrading siteready:
AFDOCS_VERSION=<version> siteready <url>.
v1.16.0
Fixed: markdown-negotiation middleware returned 404 to agents (Cloudflare Pages, Netlify)
The middleware enhance writes for Cloudflare Pages (functions/_middleware.js) and Netlify
(netlify/edge-functions/) treated every markdown-preferring request as a page request. For any
URL that already has a file extension it looked up <path>.md, found nothing and served the
/404.md mirror with a 404:
.mdURLs requested withAccept: text/markdown(Claude Code's WebFetch sends
text/markdown, text/html, */*), so the.mdlinks yourllms.txtadvertises were broken for
those clients.robots.txt,sitemap.xmland nestedllms*.txtindexes requested by an AI crawler UA
(GPTBot, ClaudeBot, ...).
The templates now skip negotiation for any path with a file extension. Claude-User (Claude's
user-initiated fetcher) is added to the bot list. Vercel's Proxy was not affected.
If you already ran enhance: it never overwrites an existing middleware, so your site still
has the old one. Either delete the generated file and re-run siteready enhance, or add this
guard before the markdown branch:
const url = new URL(request.url);
const hasExtension = /\.[a-z0-9]+$/i.test(url.pathname);
if (wantsMarkdown && !hasExtension) {New: agent-headers scanner (on by default)
The external scanners test one header pattern per URL type and follow redirects, so they all
passed the broken middleware above. agent-headers requests every kind of URL your llms.txt
links to (index files, .md links, pages, plus robots.txt/sitemap.xml, following nested
llms*.txt one level) with real agent headers (Claude Code's Accept, a GPTBot UA) and without
following redirects. A check fails when a URL that works for a plain request returns an error, or
redirects to a different path, for an agent.
It's plain fetch() with no hosted API behind it, so it runs by default for scan/rescan and
for scan-local/rescan-local/loop. Run it alone with --scanners agent-headers.
v1.15.0
Restored: the loop command
scan-local -> enhance -> rescan-local, chained into one command with no manual step in between —
an opt-in single-shot convenience for a quick pass (e.g. a POC). The three-separate-commands path
stays the default recommendation, since it lets you review the enhance diff before re-scanning.
loop was removed in v1.14.0 over a theory that two Quick Tunnels opened back-to-back were
unreliable. That theory turned out to be wrong: the real cause was tunnel.js probing a
freshly-minted tunnel hostname before it was actually safe to. Fixed directly (gate the first probe
on cloudflared's own "precheck complete" signal) — two tunnels in one process are now no less
reliable than one, so loop came back.
Side effect of chasing that fix down: is-agentic's long-standing "could not fetch homepage" error
against subfolder-hosted sites (e.g. a GitHub Pages project site) is now confirmed to be a
scanner-side bug that breaks on any non-root path — not something siteready can fix, but no longer
a mystery either.
New: local-scan vs. deployed-URL score gap, documented and surfaced
scan-local/rescan-local/loop only run a platform's real edge runtime locally for Cloudflare
Pages and Vercel. Every other platform (Netlify, GitLab Pages, generic static-host fallback) gets a
plain static-file server, which can't execute that platform's edge functions — confirmed
concretely for Netlify, whose markdown-negotiation Edge Function fix can only be verified against a
real deploy. A local scan now prints a clear note when this applies, and the same note is written
into report.md/report.json (localScanNote) — instead of the local score just reading lower
with no explanation.
Fixed
- A latent bug in
loop(present before its v1.14.0 removal too, not introduced here): it was
missing the same Jekyll guardscan-localalready had. - Stale documentation describing the reverted Quick Tunnel min-gap guard as still-current behavior
(docs/architecture.md,SKILL.md, CLI help text).
Full history: WORKLOG.md.
v1.14.0
Quick Tunnel reliability + loop command removed
Tunnel reliability fix: traced Cloudflare Quick Tunnel's DNS-propagation flap to spacing
between tunnel creations, not per-attempt delay. src/lib/tunnel.js now refuses to open a new
tunnel under a 2-minute gap since the last one (persisted across CLI invocations), with a clear
error naming how long to wait -- an evidence-based known-bad-zone filter, not a claim that 2
minutes is proven sufficient. Reachability-flap failures no longer retry back-to-back with no gap.
Added a rolling tunnel-count warning as usage nears the observed (undocumented) ~20/hour creation
rate limit.
Removed the loop command. Its whole design -- chaining baseline-scan -> enhance -> re-scan ->
diff-report into one automatic shot -- was exactly the back-to-back-tunnel pattern just proven
unreliable for hosted scanners (is-agentic/ora). No deprecation, since there are no real users
yet. New rescan-local command (scan-local's rescan counterpart) means the reliable replacement
workflow now exists: scan-local -> enhance -> rescan-local, run as three separate commands so
a real gap (e.g. reviewing the report) naturally falls between the two tunnel-needing steps.
Changed
src/lib/tunnel.js: persisted min-gap guard, reachability-flap-aware retry loop, rate-limit
proximity warning.src/cli.js: newrescan-localcommand;loopcommand removed.src/loop.js:runLoopremoved (keptrunScanLocaland its supporting helpers).scripts/verify-loop.js: rewritten to composerunScanLocal+enhance+buildDiffReport
directly instead of calling the removedrunLoop.- README.md, docs/, SKILL.md, AGENTS.md, CONTRIBUTING.md: every
loopreference replaced with the
scan-local/enhance/rescan-localsequence.
Verified: npm run verify-loop green across all three reference fixtures, scripts/check-syntax.js clean.
v1.13.0 — GitLab Pages platform module
Third step toward "support as many framework/platform combos as possible, zero new accounts or auth" (NEXT_ACTIONS.md #20-#24).
#22 — GitLab Pages platform module
New src/platforms/gitlab-pages.js — static-only warning fixer, same shape as github-pages.js (no server-side Accept-header branching possible on either static host).
detect-stack.js gained hasGitlabPagesJob(), detecting .gitlab-ci.yml + a top-level pages: job as its own platform signal (no YAML parse dependency, matching the existing config-file-existence style used for every other platform signal). Applied to both the npm-ecosystem branch (astro/astro-starlight + gitlab-pages is now a supported enhance pairing) and the Jekyll branch (previously hardcoded to github-pages, now gitlab-pages or github-pages depending on the CI config present).
loop.js's runScanLocal guard was generalized from a per-platform allowlist to excluding Jekyll by framework (its build is Ruby/bundler, not npm run build, regardless of which Pages host it targets) — so gitlab-pages, and any future platform detect-stack.js learns, rides scan-local automatically with no allowlist to keep extending.
Docs (README.md, docs/install.md, docs/why.md, docs/architecture.md) updated for this and the previous release's Netlify + generic static-server changes.
Verified: detectStack() across 5 synthetic repos (including 2 regression checks confirming plain Jekyll and plain Astro behavior is unchanged), idempotent enhance(), runScanLocal correctly rejects Jekyll regardless of platform, npm run verify-loop green.
Full detail: WORKLOG.md v1.13.0.