v1.4.0
Added
astro-seo.md(EN/JP) — Astro-specific SEO layer:client:*directives mapped to INP/LCP
costs,server:deferserver islands, canonical and Open Graph generation from
Astro.site+Astro.url, JSON-LD generated from Content Collections schemas (with
</script>escaping),@astrojs/sitemapconfiguration including multilingual sitemaps with
hreflang, view transitions (native vs.<ClientRouter />) and their analytics pitfalls, and
version migration notes for the SEO-affecting breaking changes in Astro 7 (Rust compiler HTML
validation,compressHTML: 'jsx'collapsing whitespace in indexable text, the Sätteri Markdown
pipeline) and Astro 6edge-seo.md(EN/JP) — Cloudflare Workers/Pages SEO layer:_redirectsdesign and limits,
_headerswithX-Robots-Tagand cache strategy, the static-asset vs. Worker-code blind spot,
dynamicsitemap.xmlgeneration from D1/KV with edge caching, crawler verification by published
IP ranges and Web Bot Auth, HTMLRewriter metadata rewriting and JSON-LD injection, and the
relationship between edge caching and crawl budgetlast_verifiedfrontmatter on every skill markdown file, enforced by CIdocs/research-notes.md— the verification log behind every factual claim, with source URLs.github/workflows/freshness-reminder.yml— opens a freshness-check issue on the 1st of each
month with the list of sources to re-verify.github/workflows/release.yml— publishes a GitHub Release onv*tags, using the matching
CHANGELOG section as the release notes- Versioning policy section in both READMEs, and Astro / Cloudflare Workers/Pages in Supported
Frameworks
Changed
- Skill descriptions now include Astro and Cloudflare Workers/Pages invocation triggers, and state
which of the two skills to load: English input selectsseo-mastery, Japanese input selects
seo-mastery-jp. Previously the two descriptions were near-identical, so either could fire SKILL.md(EN/JP) gained an "Astro and Edge (Cloudflare) Specifics" section, registered the two
new reference files, and now states that topic files and platform files must be read together —
sitemaps, hreflang and crawl budget each span more than one fileaudit-workflow.md(EN/JP) points at the platform files, since several audit phases surface
findings whose cause is platform-specific- README positioning no longer claims freshness is "guaranteed": what is guaranteed is the process
and the date stamp. Supported Frameworks now links each entry to the file that backs it, and drops
the WordPress entry, which had no supporting content anywhere in the repo CONTRIBUTING.md's Japanese section is a full translation rather than a summary; it previously
omitted the repository layout, the one-language-PR rule, the freshness workflow, and all four
content guidelinesmarketplace.jsongainedastro,cloudflare-workersandedge-seokeywords- CHANGELOG restructured to Keep a Changelog format; historical entries re-dated from git history
(the v1.0.0 entry previously read "2025-01"; the initial commits are dated 2025-12-29/30) scripts/validate.pynow verifieslast_verifiedon every skill markdown file (present, ISO
format, not in the future, identical between EN and JP) and ignores#lines inside fenced code
blocks when comparing document structure
Fixed
- AI search eligibility was incomplete.
ai-search.mdandSKILL.mdstated that indexability
plus snippet eligibility were the only requirements for AI Overviews / AI Mode. Google's
generative AI optimization guide (2026-05-15) adds a third: the site must be included in Search
generative AI features in Search Console. Documented the Search generative AI control
(Settings → Search generative AI, rolled out 2026-06-03), its three options, and the fact that
Google states it "isn't used as a ranking or inclusion signal affecting other parts of Search" —
making it the correct tool for AI-only exclusion, unlikenosnippet ai-search.mdnow cites the 2026 generative AI optimization guide, including its verbatim
mythbusting on llms.txt, content chunking, AI-specific writing, and AI-specific schema- Search generative AI performance report described with its actual limits: no historical backfill,
impressions only. The 2026-05-18 data start is attributed as a secondary-source figure, since
Google's own documentation does not state one technical-seo.mdsitemap best practices: noted that Google ignores<priority>and
<changefreq>, thatlastmodis only used when consistently accurate, and added the sitemap
index limitstechnical-seo.mdcrawl budget: removed "parameter handling (Search Console)" — the URL
Parameters tool was retired in 2022technical-seo.mdgained the robots.txt limits Google actually enforces (500 KiB, 24-hour cache,
crawl-delayandnoindexunsupported, longest-match precedence)technical-seo.mdNuxt SSR example replaced Nuxt 2asyncData+$axioswith Nuxt 3+
useAsyncData/$fetchcore-web-vitals.mdNuxt example replacedrender.http2.push(HTTP/2 Server Push, removed from
Chrome) with a Nuxt 3+@nuxt/imageconfig and explicit LCP preloadcore-web-vitals.mdcorrected thescheduler.yield()support note: Chromium and Firefox, not
Safari, not Baselineaudit-workflow.mdrobots.txt check no longer asks whetherCrawl-delayis set too high; it now
flagscrawl-delay/noindexlines as ineffective for Googleaudit-workflow.mdAI readiness phase now checks the Search Console generative AI control
Found by review of this branch before release:
- Article
headlinehas no 110-character limit. Google removed it from the documentation in
January 2023; the current wording only warns that long titles may be truncated. The claim was
presented as a Google requirement instructured-data.md(since v1.0.0) andastro-seo.md _redirectsplaceholders do match dots inside the path. The delimiter is/in the path and
.or/only in the host, so/:slugalso matchesreport.pdf— the previous wording made
such rules look safe for file URLs_headersrules accumulate rather than override, joining repeated header names with a comma.
The per-crawlerX-Robots-Tagexample would therefore have shipped one combined header that
Google does not document parsing; removed, with the collision rule documented. The/*.html
cache rule was dropped too — underauto-trailing-slashit matches nothing- Middleware-style Workers need
run_worker_first. Cloudflare serves a matching static asset
before invoking the Worker, so the HTMLRewriter and query-canonicalization examples could never
have executed under the config shown - Supplied the
ipInCidrimplementation the crawler check called but never defined; made the range
lookup fail open rather than 500 on a fetch error; stopped an empty KV value poisoning the cache - Dynamic sitemap generation no longer 500s on a NULL or non-ISO
updated_at, strips
XML-illegal control characters, and cannot emit a zero-entry sitemap index - HTMLRewriter handlers now catch their own exceptions (a throwing handler errors the response
body), guard missing values, and scope the selector tohead > titleso inline SVG titles are
left alone - Removed the unsourced claim that 410 is processed faster than 404; attributed the generative AI
report's 2026-05-18 data start as a secondary-source figure and removed it fromSKILL.md;
replaced "expanding from July 2026" and "takes effect within 1–2 days" with Google's own wording freshness-reminder.ymldeclared onlyissues: write, which sets every other scope tonone—
actions/checkoutwould have failed on the very first runvalidate.pycrashed with a traceback on a syntactically valid but impossible date, could fail in
CI for a file stamped "today" in JST, mispaired indented code fences, and reported one malformed
file three times. It also now checks that every declared version agrees — v1.2.1 and v1.2.2
shipped with both manifests still reading 1.2.0release.yml's empty-section guard could never fire (the extracted section always contains a
newline), the notes regex now terminates on end-of-file rather than relying on the link-definition
block, and a re-run updates the release instead of failing on "already exists"
Found by a second review pass on the same branch:
- The dynamic sitemap published 404s for hierarchical slugs.
locFor()ran
encodeURIComponent()over the whole slug, so2026/my-postbecame2026%2Fmy-post— a
different URL, submitted to Google as canonical. It now encodes each path segment astro-seo.md's blog route overrode the canonical with a hardcoded trailing slash. The
getStaticPathssample built`/blog/${post.id}/`and passed it ascanonicalUrl, so under
trailingSlash: 'never'every post declared a canonical andog:urlthat did not match the URL
it was served at — the two-URLs-per-page split the next section warns about. The sample now lets
BaseLayoutderive the canonical fromAstro.url.pathname- The tracking-parameter 301 rewrote parameters it was not meant to touch. Mutating
url.searchParamsre-serialises the whole query, turning?q=a%20binto?q=a+b; an origin that
normalises it back would bounce the request between the two rules. It now rebuilds the query from
the raw pairs - The sitemap Worker called
cache.put()without checking the method, so a HEAD from any crawler or
uptime monitor rejected insidewaitUntil; it also ran a cache lookup before routing, on every
static asset request, and spent a secondCOUNT(*)query per page purely to bound the page number astro-seo.mddescribed a missingsiteas silently producing broken canonicals.new URL(path, undefined)throws, so the build fails — and only one of the three components that dereference
Astro.sitehad an explicit guard, which made the other two look like the silent case- The
@astrojs/sitemapserializeexample demonstratedpriority, which the paragraph below it
says Google ignores, and matched only the blog index rather than the posts. It now setslastmod,
the one field the file says is worth getting right validate.pyrejected valid frontmatter: block scalars, sequences, nested mappings and multi-line
plain scalars all errored as "notkey: value", so adding any standard multi-line field would
have failed CI. It also missed a value ending in a colon — the same scanner error the plain
scalar check exists to catch — false-positived on values starting with-,?or:, which YAML
accepts, and left\uXXXXescapes unexpanded while measuring description length against themrelease.ymlre-implemented frontmatter parsing insed, which disagreed withvalidate.pyover
quoted values:version: "1.4.0"passed CI and would then have failed the release, after the tag
was already immutable. The tag check is nowvalidate.py --expect-versionfreshness_table.pywalked the skills tree recursively whilevalidate.pyglobbed one level, so
a file in a subdirectory would be demanded in the reminder issue but never enforced by CI. Both
now walk through the same function, the table exits non-zero on malformed frontmatter instead of
rendering it as "missing", and CI runs the script so a rename on its import cannot go unnoticed
until the monthly workflow fires