overhaul: SEO completion, AI SEO, analytics - #26
Open
CS-5 wants to merge 2 commits into
Open
Conversation
Sitemap: the /styleguide special case becomes one rule — a page that carries noindex is not in the sitemap. That covers a retired event too, since its route is a redirect page Astro writes with the same tag, and needs no second list of what is hidden. astro:content is unreachable from astro.config.ts, so the filter reads the emitted HTML; the sitemap integration runs after every page is on disk. robots.txt is allow-all with a Sitemap directive, and _headers keeps only the three preview/staging noindex rules (D25). ADR 0009 records the posture, AI crawlers included, and the check that nothing private is on the domain. verify-meta.mjs asserts every built page's head: one unique title, one unique 50-160 character description, one canonical, one h1, a resolvable absolute og:image with alt, and JSON-LD that parses with a schema.org @type. It failed four pages on its first run; /sponsors/ needed its description separated from its hero copy, which is legacy's verbatim. /llms.txt is collection-backed, so a retired event leaves it the same build it leaves the sitemap. CI link-checks it, which is what keeps src/data/events.ts honest about where each entry is published. Seven social cards from one template (ADR 0010), replacing a 456 KB PNG shared by every page with 34-52 KB per section. Analytics: GA4 via gtag after load, behind both a PROD gate and a production-hostname check, with Consent Mode v2 defaults set before config. The event taxonomy is a delegated listener that reads link destinations from site.ts rather than twenty data-track attributes; data-track still covers a sponsor's own URL, and the contact form dispatches its own event. Cloudflare Web Analytics is wired but has no token yet — PUBLIC_CF_BEACON_TOKEN, empty by default, and the beacon is skipped without it. Owner tasks that need account or dashboard access are in plan/todo.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CxssDiw8XBERsuHMQZTtCr
Review outcomes from the project owner. An event now goes out of service once its `end` has passed, so last season's page stops being live content on the first deploy after the event rather than waiting for someone to set `hidden: true`. The rule is `hasPassed`, moved into event-date.ts and shared with Countdown, which already had exactly this definition — two copies would have let a page say "this event has passed" while still sitting in the sitemap. Everything downstream already handled it: the route redirects, the sitemap filter drops the redirect page because it is noindex, and /llms.txt drops it because it reads getVisibleEvents. Both current entries are dated in the past, so this ships with no live event page. Two knock-ons handled: /llms.txt omits an empty section rather than printing a bare heading, and lighthouserc.json budgets /about/ where it budgeted /openhouse/, because a seasonal URL cannot be a stable budget target. plan/00-overview.md's client-JS budget now reads "first-party" rather than "including analytics" — gtag.js is about 35 KB gzipped on its own, so the original wording and D21 could not both hold. docs/analytics.md says what the Lighthouse gate does and does not measure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CxssDiw8XBERsuHMQZTtCr
CS-5
force-pushed
the
overhaul/10-seo
branch
from
September 1, 2026 13:54
c52e08b to
18a0842
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 10 of the stacked overhaul (
plan/10-seo.md), stacked on #25 per D23.What this does
Sitemap & robots. The
/styleguidespecial case inastro.config.tsbecomes one rule: a page that carriesnoindexis not in the sitemap. That covers a retired event too — its route is a redirect page Astro emits with the same tag — so there is no second list of what is hidden.robots.txtis allow-all with aSitemap:directive;_headersis down to the three preview/stagingX-Robots-Tagrules (D25).verify-meta.mjs, run in CI after the build: every built page gets exactly one unique<title>, one unique 50–160 character description, one canonical, one<h1>, an absoluteog:imagethat resolves to a real file plus its alt, and JSON-LD that parses with a schema.org@type. Redirect pages are skipped./llms.txt, collection-backed, so a retired event leaves it the same build it leaves the sitemap. CI link-checksdist/llms.txtalongside the HTML.Seven social cards from one template (photo, scrim, eyebrow, Orbitron title, lockup), replacing the single 456 KB PNG every page shared with 34–52 KB per section.
Analytics (D21). GA4 via a plain
gtagsnippet afterload, behind bothimport.meta.env.PRODand a production-hostname check, with Consent Mode v2 defaults set beforeconfig. Cloudflare Web Analytics is wired but has no token yet — see the gaps below.Events retire themselves once their
endhas passed — requested during review, see below.Things worth a reviewer's attention
§1's
getVisibleEvents()filter is not possible.astro:contentis a virtual module of the build's own module graph, andastro.config.tsis loaded before it exists. The filter reads the emitted page instead, which is a single source of truth (the page's ownnoindex) and covers anything that grows the tag later. Safe because@astrojs/sitemapfilters inastro:build:done, after every page is on disk.verify-metafailed four pages on its first run, which is the point of it:/donate/(173 chars),/programs/frc/(162),/sponsors/(195),/openhouse/(288). Three were trimmed./sponsors/is the one to look at — its description was also its hero copy, which is legacy's verbatim under D8, so the two are now separate strings: one written for the page, one for a search result.Analytics reads link destinations, not
data-trackeverywhere. §7 specifiesdata-trackattributes. Twenty call sites link to/get-involvedalone, and tagging each would be a second list to keep in step withsrc/data/site.ts— the exact pattern the midpoint review's first standing rule exists to prevent. The delegated listener maps destination → event name from a table built out ofsite.ts;data-trackis still read first, for the two clicks a URL cannot identify (a sponsor's own site, fromSponsorCardandSponsorStrip).contact_submithas neither a click nor a destination, soContactFormdispatches a custom event whose name both ends import fromsrc/lib/analytics.ts./llms.txtneeds a route manifest. Aneventsentry cannot know its own URL —/openhouseand/programs/frc/kickoffare hand-written routes, not a[slug].src/data/events.tspairs id to path and the endpoint throws at build on an entry with no route; the other direction is covered by the link check.The social cards are committed artifacts, not a build step (ADR 0010).
sharpresolves an SVGfont-familythrough fontconfig, which reads neither the variable woff2 the site ships nor a weight axis — so a build-time pipeline would need a font cache on every build machine, Cloudflare's included, to render files that change about never.pnpm assets:og-fontsthenpnpm assets:og, by hand.Review outcomes
Two things the project owner settled after the first push:
1. Events retire on date.
inService()insrc/lib/events.tsnow also excludes an entry whoseendhas passed, so last season's page stops being live content on the first deploy after the event rather than waiting for someone to sethidden: true. Everything downstream already handled it: the route redirects to its parent, the sitemap filter drops the redirect page because it isnoindex, and/llms.txtdrops it because it readsgetVisibleEvents().The rule is
hasPassed(end), moved intosrc/lib/event-date.tsand shared withCountdown, which already had exactly this definition — two copies would have let a page say "this event has passed" while still sitting in the sitemap. An entry with noendnever retires on its own, which is the ruleCountdownestablished in Phase 08 and the reasondocs/content.mdtells editors to always set one.hidden: trueremains the way to retire one early.Both current entries are dated in the past, so this ships with no live event page. Verified:
/openhouse/and/programs/frc/kickoff/are redirect stubs, both are absent fromsitemap-0.xml,/llms.txtomits the Events section rather than printing a bare heading, and nothing in the built HTML links to either. Two knock-ons handled —lighthouserc.jsonbudgeted/openhouse/as its event-landing page shape and now budgets/about/, because a seasonal URL cannot be a stable budget target; and the countdown's "passed" state is now only reachable between an event ending and the next deploy.2. The client-JS budget excludes analytics.
plan/00-overview.mdbudgeted "< 35 KB gzipped per page including analytics", whichgtag.jscannot fit alone — that wording and D21 could not both hold. The overview now reads "first-party", anddocs/analytics.mdstates what the Lighthouse gate does and does not measure: first-party JS is 0.7–3.1 KB per page, inline in the document; production addsgtag.jsafterload, where it never touches LCP or TBT.Intentional gaps — owner tasks, tracked in
plan/todo.mdThese are finished as far as this repository can take them.
cloudflareBeaconTokeninsite.analytics"with its real value"; there is no value to commit. It is aPUBLIC_CF_BEACON_TOKENenv field instead, defaulting to empty, with the beacon skipped on an empty token — the same shape asPUBLIC_TURNSTILE_SITE_KEY, so it is set in the Pages dashboard rather than in a commit. D21 is not fully satisfied until that happens.validator.schema.orgare both Google-hosted, and this environment's network policy returns 403 for*.google.com(confirmed against the agent proxy's status endpoint). The five shapes —NGO,WebSite,BreadcrumbList,Event,FAQPage— were inventoried and asserted locally instead: every block parses, carries@context: https://schema.org, and has an@type, now checked in CI. Google's own eligibility rules still need a run.docs/analytics.mdsays what to change and why.src/assets/. Swap a path intools/assets/og-cards.mjsand runpnpm assets:og.Verification
pnpm check && pnpm buildgreen;verify-metagreen across all 16 pages; Lighthouse gate green on all six URLs with SEO 100 and every LCP run under budget.localhost: consent defaults,js, andconfigland in that order;gtag.jsis injectedasync; the header CTA pushesget_involved_click, a/donatelink pushesdonate_click, an/aboutlink pushes nothing; the dispatched event arrives ascontact_submit. Without the rewrite, onlocalhost, there is nodataLayerand no tag request on any page. Transcript indocs/analytics.md.h1each, no skipped heading level anywhere, no bare "click here"/"read more" link text, no page errors.<time datetime>was missing only from the surface that builds its dates in the browser — the calendar agenda — and now carries it on the day heading and each entry's time range, with an all-day entry stamped as a date rather than a midnight timestamp. Verified against a stubbed feed.src/content/andsrc/pages/hold marketing copy, sponsor records, robot history, a published FAQ, and a public event's published schedule. ADR 0009 records the posture — AI crawlers explicitly not blocked, and why the legacy/imageand/videoDisallowrules were costing image and video search traffic for a risk nobody could name.