Uh oh!
There was an error while loading. Please reload this page.
feat: complete the Astro migration, remove all framework islands - #199
Open
sinduri-g wants to merge 120 commits into
Open
feat: complete the Astro migration, remove all framework islands#199sinduri-g wants to merge 120 commits into
sinduri-g wants to merge 120 commits into
Conversation
- add nested astro/ app: static output, trailingSlash always, base from VITE_BASE_PATH - serve real public/ via publicDir; read real adventure YAML via glob content collection - port root.tsx head into Layout.astro: CSP, favicons, theme pre-hydration + after-swap reapply, GA4 consent bootstrap, dual JSON-LD, base-aware font preloads - add ClientRouter view transitions and native prefetch (replaces speculationrules script) - prove YAML -> Zod -> createMarkdownProcessor + rehype-sanitize field HTML at build time - port index.css (@theme) verbatim; wire Tailwind 4, astro-icon, Vue appEntrypoint - stub nanostores theme/consent persistent stores Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
Contributor
|
- add **/*.astro and **/*.vue globs to REUSE.toml MIT block (reuse-tool does not auto-recognize .astro, so reuse lint fails on the scaffold otherwise) - exclude astro/** from root eslint; the nested app imports astro:* virtual modules that root tooling cannot resolve, and gets its own lint later Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…e 2) - translate schemas/adventure.schema.json to a strict Zod schema (unknown fields fail the build, preserving the ajv validation gate) - custom yaml-package loader instead of glob(): Astro's glob YAML parser auto-casts unquoted ISO deadlines to Date; the yaml package keeps them as strings, matching the generator. digest-gated - port the generator's markdown pipeline verbatim (sanitize schema, abbr-tooltip expansion, external-link annotation, non-public-url unwrapping, md-inline/md-content) - port field normalization: title/name, story, icon/emoji, difficulty/emoji, learnings aliases, intro/summary, codespacesUrl, discussionUrl, deadline, rewards defaults - add verify-gate.mjs: renders real fields through the pipeline and diffs against committed *.generated.ts (155 matched, 0 mismatched) - defer to phase 2b (TODO markers): architectureDiagram assets, meta-description synthesis, services step injection, solutions collection, discussion/leaderboard references Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…phase 2b) - port buildAdventureMetaDescription/buildLevelMetaDescription; always emit metaDescription (synthesized when absent), matching the generator - port services -> "Explore the UIs" how_to_play step injection - extract pure derivations to src/lib/adventure-derive.mjs, shared by the loader and the verification gate - extend verify-gate to cover metaDescription and the injected services step (191 matched, 0 mismatched) - add npm scripts: sync (astro sync validation gate) and verify:content (byte-fidelity gate) - reclassify solutions (pre-built TS) and discussion/leaderboard (refreshed JSON) out of the content collection; they become phase 3 import.meta.glob loads Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
- add SEO.astro (canonical, OG incl article type, Twitter, fixed brand og:image); Layout delegates per-page meta to it - add lib/site.ts (SITE_URL, BRAND_NAME, OG_IMAGE_ALT, canonicalUrl) and lib/markdown.ts (stripLinks, stripHtml, slugify ported) - add /adventures/[id] detail route via getStaticPaths from the collection; prose via set:html with md-inline and stripLinks in link cards - add /adventures list page (month-sorted, stripHtml story in cards) - verified in dist: titles, canonical (trailing slash), OG/Twitter, synthesized descriptions, unescaped prose, trailing-slash routing Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…d (phase 3 increment 2) - add /adventures/[id]/levels/[levelId] via getStaticPaths (all 17 level routes) - render hook, intro, scenario, objective, toolbox, walkthrough, verification, helpful links, audience via set:html; abbr tooltip expansion confirmed on a real page - load discussion posts and leaderboard at build time via node fs (community-data.ts) -> fully static, no client fetch - resolve external data dir from process.cwd(), not import.meta.url: page-imported modules are bundled by Vite which rewrites import.meta.url Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…nt 3) - add /challenges/[...tag] (base + 25 tag routes) via getStaticPaths from the collection - add ChallengesFilter.vue island (client:load): SSR renders the full/pre-filtered grid so no-JS users get content, hydrates for topic/difficulty filtering - URL-synced state via history.replaceState (scroll-preserving); hydration-safe seed from the route tag, restore ?topics/?difficulty in onMounted - add lib/challenges.ts deriving the flat level list and tag set (mirrors filter-utils/tag-utils) - solution page deferred: pre-built solution TS import from the React src via the @/ alias; bridge at cutover Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
… increment 4) - add Navbar.astro (nav landmark, links, active state) and Footer.astro (Explore/Community landmarks, social, copyright) - wire into Layout with skip-nav, new-tab-hint span, and focus-to-main on astro:after-swap - add ThemeToggle.vue island (nanostores $theme + lucide-vue-next): hydration-safe, persists to localStorage, client:load + transition:persist - replace placeholder home with a real landing (hero + featured adventures from the collection) - add nav/site constants to lib/site.ts; remove scaffold smoke island Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…ment 5)
- port the full consent state machine from useConsent.tsx to stores/consent.ts: plain nanostores atom (hydration-safe, default null), React storage format ({value,timestamp}+180d expiry), gtag injector (queue update/js/config before appendChild, module-scoped once-guard), revoke, GPC auto-deny, _ga cookie clearing, firePageView on astro:page-load
- add ConsentBanner.vue (client:load + transition:persist): SSR renders the banner with aria-live; floating cookie-preferences button after a decision
- mount in Layout; gated load verified statically (zero gtag.js prerendered)
- NOTE: runtime transitions (accept/decline/GPC/re-grant/expiry) still need real-browser regression tests before merge
Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>…rement 6) - port Hero.astro (fireflies, hero-badge, brand slogan/line, animate-fade-up) and AdventureCard.astro with sub-components DifficultyBadge, ContributorBadge, LivePill, AdventureIcon - add lib/difficulty.ts and brand slogan constants - use Astro class:list in place of cn; astro-icon (lucide, inline SVG, zero JS) in place of lucide-react - home page uses Hero + card grid; /adventures uses the same cards; build-time isLive Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
… (phase 3 increment 7) - port FilteredLevelCard design into ChallengesFilter.vue: card-glow, difficulty badge with dot, learnings, estimated-time pill, live pill, adventure icon - inline badge/pill/icon markup with lucide-vue-next since .astro components can't render inside a Vue island - extend challenges.ts ChallengeEntry with learnings, adventureIcon, and build-time isLive Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
- port the two-column AdventureDetail layout: breadcrumb, header (title/icon/contributor badge/month/live pill/tag chips/story), main column (overview, challenge level cards, rewards, backstory), sidebar (leaderboard + contributor) - add components: Breadcrumb, TagChips, InlineProse, PersonNameLink, RewardsCard, and lib/utils.ts (formatDeadline, isDeadlinePast) - reuse ported AdventureIcon/DifficultyBadge/ContributorBadge/LivePill; build-time deadline gating for live pill and rewards placement Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
- add src/pages/404.astro; Astro emits dist/404.html which GitHub Pages serves for unmatched paths as a real HTTP 404 (no soft-404 catch-all) Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…ook (phase 3) - port four nav/footer-linked static pages to .astro with faithful content, correct SEO, and chrome via Layout - add noindex prop to Layout/SEO; privacy is noindex (kept out of the index, matching the React page) - notes: CommunityLeaders sidebar rendered as a build-time snapshot; abbr tooltip keeps CSS hover/focus, JS click/Escape toggle deferred Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
- port About (board members + challenge builders), Sponsors (sponsor data + CSS theme-swapped logo), Brand Guidelines (all 8 sections), and Presentation Templates to .astro with faithful content - copy sponsors.ts and team.ts data into astro/src/data/; add Dynatrace logos to public/brand/ - add redirects (/docs, /docs/community-guide, /community-guide -> /handbook/) via astro.config; presentation-templates is noindex - nav and footer now have zero 404s; notes: brand TOC scroll-spy and community-leaders live refresh are static snapshots Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…(phase 3) - port the ChallengeDetail two-column layout: header, objective/learnings cards, collapsible audience/story/architecture (native details, zero JS), synthetic Get Started walkthrough step, verification, toolbox/docs grid, share links, sidebar (compact rewards, codespaces, community sidebar, other levels) - add components: CollapsibleSection, CodespacesButton, OtherLevelsCard, ChallengeShareLinks, CommunitySidebar; extend RewardsCard with the compact variant - wire architectureDiagram SVGs (copied to astro/src/assets/diagrams, matched by filename) with architectureAscii fallback - content gate still 191 matched, 0 mismatched Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
- bridge the React src solution modules with two exact-find Vite aliases (@/data/solutions/types, @/data/adventures/contributors); astro's own @/ alias unaffected - add lib/solutions.ts (import.meta.glob over src/data/solutions) and the /adventures/[id]/levels/[levelId]/solution route with getStaticPaths over authored solutions only - port the SolutionDetail layout: spoiler warning, step nav, context card, details step cards with takeaways, block renderer (text/code/image/callout), final result, outro, sidebar - build-time deadline gating with locked-state fallback (noindex) Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…hase 5) - add Playwright + axe a11y suite (e2e/a11y.spec.ts) over 15 representative routes: axe dark/light/forced-colors with the full WCAG tag set, touch targets (2.5.8), focus rings dark/light, focus traps, 200% zoom reflow. 105 tests pass - reuseExistingServer:false so tests never run against a stray dev server - fix heading order: sr-only h2 before card grids on /adventures and /challenges - fix touch targets: nav links min-h-[44px], footer links min-h-[48px], clear-filters button min-h-6 - fix 200% zoom reflow: abbr tooltip ::after uses display:none when hidden (was opacity:0, which kept layout and extended page width); the React JS tooltip that suppresses this CSS is a follow-up - constrain walkthrough step grid track with minmax(0,1fr) so code blocks scroll internally Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
- add e2e/smoke.spec.ts: every route has a unique correct <title>, canonical (SITE_URL+path), meta description, matching og:title/og:url, exactly one <h1>, and no page errors - island hydration checks: theme toggle switches theme + sets .light class; challenges filter filters and syncs ?difficulty - 19 tests pass Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…tover stage 1) - deploy.yml: output dist/ (not dist/client/), drop the 404.html copy (astro emits dist/404.html natively), keep JamesIves/github-pages-deploy-action - preview.yml: check job runs sync + verify:content gates; build/e2e/preview use dist/; e2e runs playwright (webServer serves via astro preview); preview build uses astro base (VITE_BASE_PATH) so public assets deploy from dist/ with no per-directory copy step - these target the post-move root structure; inert on the branch until the cutover move lands and the branch is merged - TODO(cutover): add npm run lint once eslint-astro/vue config is ported Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…e (stage A) - copy adventure YAML, discussion/leaderboard JSON, solutions, contributors.ts and types.ts into astro/src/data (the app no longer reads the React src/data tree) - re-point content.config (data/adventures), community-data (cwd/src/data/adventures), and solutions (../data/solutions) to the local copies - drop the two interim Vite aliases; @/data/solutions/types and @/data/adventures/contributors now resolve via the app's @/ -> src alias - build green (63 pages), content gate 191/0, all 124 e2e tests pass Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…age B) BREAKING: the React Router app is removed; offon.dev now builds from Astro. - delete the React app: src/ (root.tsx, entry.*, routes.ts, ~50 .tsx pages/components, hooks), react-router.config.ts, serve.json, the React vite/vitest/eslint/tsconfig configs, e2e/, and the shadcn components.json - delete the content generator: scripts/generate-adventures.mjs, generate-solutions.mjs, create-data-aliases.mjs, schemas/adventure.schema.json, and all *.generated.ts / index.ts / summaries.ts (replaced by the Zod content collection) - move the Astro app from astro/ to the repo root (src/, astro.config.mjs, package.json, tsconfig.json, e2e/, playwright.config.ts) - drop publicDir override (public/ is now local); remove the obsolete verify:content gate (nothing left to diff against) and its verify-gate.mjs - preview.yml: content gate is now `astro sync` only - kept: public/, refresh-*/sync/community-sitemap/check-docs scripts, docs, LICENSES, REUSE.toml - verified at root: build (63 pages), astro sync, 124 e2e tests (105 a11y + 19 smoke) all pass - follow-ups: rewrite sync-adventure/add-discussion-url/validate-adventures workflows (still reference the removed generator), add eslint-astro/vue lint, custom sitemap endpoint, update CLAUDE.md/README/REUSE.toml, port abbr JS tooltip + Shiki highlighting + click tracking Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…tover follow-up) - replace the deleted `generate-adventures.mjs --validate-only` with `npm run sync` (astro sync runs the Zod content schema and fails on invalid adventure YAML) - drop the obsolete checks: generated TS existence, index.ts import, react-router prerender entries, and sitemap.xml grep (routes now come from getStaticPaths; sitemap endpoint is a follow-up) - keep: per-level discussion JSON existence and ADVENTURE_CATEGORIES registration in refresh-leaderboard.mjs - update path triggers (src/content.config.ts instead of schemas/react-router.config) Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…nerator (cutover follow-up) - replace `node scripts/generate-adventures.mjs` with `npm run sync` (Zod validation) in both workflows - commit only the collection inputs (adventure.yaml, *-posts.json, diagrams, refresh-leaderboard.mjs); stop staging the removed *.generated.ts / index.ts / summaries.ts - rewrite PR-body prose: routes/sitemap now come from getStaticPaths at build time (nothing to regenerate); checks use `npm run sync && npm run build && npm run test:e2e` Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…llow-up) - replace all React Router / generator guidance with Astro: stack, repo layout, commands, content collection, components/islands, hydration safety, SEO (<SEO> component), deployment (dist/, trailingSlash, PR-preview base) - rewrite the analytics/consent section for the nanostores $consent store + Vue island (state machine preserved); update constants home to src/lib/site.ts - preserve framework-agnostic rules verbatim: Content & Copy, Git, Brand, URLs, Stability, Debugging evidence rules - add a Known follow-ups section (lint, sitemap endpoint, abbr JS, Shiki, click tracking, REUSE cleanup) Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…llow-up) - README: Astro + Vue stack, commands (astro build/sync/test:e2e, dev port 4321), project structure, content collection, routes (.astro + getStaticPaths), SEO (<SEO> component), analytics (nanostores), deployment (dist/, base) - REUSE.toml: drop entries for deleted files — *.generated.ts (block 3 + per-contributor blocks), src/components/ui + shadcn block, and community-leaders.json (CC0 block); src/lib/utils.ts now falls under the MIT **/*.ts glob Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…tion (cutover follow-up) - ADVENTURES.md: rewrite the content pipeline (YAML -> content collection, no generation), the sync-workflow steps, the solutions flow (import.meta.glob, no barrel), the checks command (npm run sync && build && test:e2e), the validate-adventures row, and the constants pointer (src/lib/site.ts) - PERFORMANCE.md: add a migration note (Layout.astro preloads, Astro islands code-splitting, content-collection markdown, native prefetch, dist/ output, getStaticPaths) — principles unchanged - styleguide.md: add a migration banner (design system stays authoritative; component entries describe the former React impl; .astro/.vue components are source of truth). Full component rewrite tracked as follow-up Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…follow-up) - add eslint.config.js (flat): @eslint/js + typescript-eslint + eslint-plugin-astro + eslint-plugin-vue; ignore dist/.astro/node_modules/public/.claude; quiet purely-stylistic vue formatting rules; keep no-unused-vars as error - pin typescript to 6.x (typescript-eslint does not support TS 7 yet); build still green on TS 6 - add `npm run lint` script and run it in preview.yml's check job (removes the cutover TODO) - fix the two real lint errors: unused `base` in the level page and unused `SITE_URL` import in brand.astro Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…r follow-up) - add src/pages/sitemap.xml.ts endpoint: enumerates home, /adventures/, adventure + level routes, solution routes, /challenges/ + tag routes, and the indexable static pages, with a build-date lastmod - exclude /privacy/, /presentation-templates/ (both noindex), and /404/ - delete the stale hand-maintained public/sitemap.xml (the generator that patched it is gone); /sitemap.xml is now generated at build (60 URLs) - robots.txt, api-catalog, and the Layout sitemap link all reference /sitemap.xml, served by the endpoint Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
- Remove all "ported from React", "ported from the generator", and "matching the React X" references across 18 files - Rewrite comments to describe what the code does, not where it came from - Preserve technically useful context (hydration rationale in consent.ts, WCAG Reflow reason in index.css, CSP constraint in AvatarLink.astro, etc.) - Delete comments that were self-evident once the migration reference was gone Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
`astro build` loads src/lib/markdown-pipeline.mjs through the content loader, so everything that module imports is a build dependency, not a dev one. The manifest disagreed: - `unified`, `remark-parse`, `remark-gfm`, `remark-rehype`, `rehype-raw`, `rehype-sanitize`, `rehype-stringify` were devDependencies. Moved to dependencies - `shiki` was imported but never declared at all; it resolved only as a transitive of astro, so any astro release that changed its shiki range would have broken the build with no manifest change. Now declared (^4.4.3) - `@astrojs/markdown-remark` was declared with zero imports anywhere in src/, e2e/, scripts/ or config. Removed Verified `npm ci --omit=dev && npm run build` succeeds, and that all eight modules resolve in the dev-omitted tree. On the previous manifest the seven remark/rehype packages were omitted by that flag and shiki was undeclared, so the same command would have failed. Full install restored: build, lint and 476 unit tests pass. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
`scripts/lib/` and `src/lib/` each held a copy and they had already diverged,
with the sync script writing deadlines through one parser and the build
reading them through the other. Only the src/ copy had the strict
unknown-timezone behaviour.
- `scripts/lib/deadline.mjs` and `scripts/lib/level-constants.mjs` deleted;
`scripts/sync-adventure.mjs` and `scripts/lib/level-sync.mjs` now import
from `src/lib/`
- `src/lib/level-constants.mjs` gains `LEVEL_DIFFICULTY_BY_ID` and
`LEVEL_ORDER`, which only the scripts copy had
- Strict `UNRESOLVABLE_DEADLINE` is the default everywhere
One thing the merge could not do bluntly: sync-adventure writes its result
back into adventure.yaml, so making it strict would have replaced an author's
human-readable deadline with `9999-12-31T23:59:59Z` in the source file and
hidden the mistake. `parseDeadline` therefore takes an explicit
`{ onUnknownTimezone: "preserve" }`, used only on that write path. Rendering
keeps the strict default, so an unparseable deadline still gates the solution
either way; a preserved string hits the sentinel when the build parses it.
Adds 7 tests covering both modes, including that a preserved value still
resolves to the sentinel at render time. 482 unit tests pass; scripts and
build verified against the shared modules.
Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>`.ai/templates/generate-reveal-zip.mjs` imports jszip, which was dropped from the manifest, so `/create-presentation`'s Reveal ZIP path failed with ERR_MODULE_NOT_FOUND. The committed zip masked it: the site was fine, only regeneration was broken. - jszip restored as a devDependency (^3.10.1, the version it had before). Not a runtime dependency: it is tooling, never imported by the site build - reveal.js deliberately left out. The script reads the deck assets from the committed public/reveal/, not from node_modules, so it is only needed for the occasional manual re-vendor. Documented as an ad hoc install, the same pattern the repo already uses for pptxgenjs - Script header and the command tables in CLAUDE.md / AGENTS.md say which of the two is installed and which is not Verified by running the script after a clean `npm ci`: emits a valid 26-file zip. Regenerated output reverted, since this was a verification run and the committed artifact is unchanged. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
Restoring axe coverage to the 12 previously-untested level routes surfaced 38 real color-contrast violations, all one root cause: both GitHub Shiki themes color comments #6a737d, which fails WCAG 1.4.3 against the code-block surfaces. - dark: #6a737d on #151519 = 3.78:1 - light: #6a737d on #f4f4f6 = 4.38:1 Both below the 4.5:1 floor. Replaced per theme with GitHub's own accessible values, #8b949e and #57606a, which measure 5.97:1 and 5.75:1. Applied via Shiki's colorReplacements so every token using that color is covered, not just the comment tokens that happened to be in the sample. Set in both places that highlight: the codeToHtml call in markdown-pipeline.mjs (adventure prose) and shikiConfig in astro.config.mjs (markdown). Note for anyone touching this: colorReplacements belongs on the codeToHtml call, not on createHighlighter's theme entries. Passing it there makes createHighlighter throw, and the catch in highlightCode swallows it and returns unhighlighted code, so the only visible symptom is that syntax highlighting quietly disappears. Verified the output still carries 6 distinct token colors and no longer contains #6a737d. 120/120 axe checks pass across 40 routes in dark, light and forced-colors. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
The migration deleted wsg.spec.ts, hydration.spec.ts and visual.spec.ts, and trimmed smoke.spec.ts, without replacing what they checked. Restored the parts that guard behaviour rather than pixels: - `budget.spec.ts`: the 750 KB page-weight budget, the third-party request allowlist (the only automated proof nothing phones home pre-consent), and the image width/height, lazy-loading and autoplay checks - `mobile-menu.spec.ts`: initial focus, Tab and Shift+Tab wrap, focus never escaping, Escape restoring focus to the trigger, `inert` + `aria-hidden` on background siblings and their cleanup, and no trap surviving navigation. Written against the DOM contract only (`button[aria-controls="mobile-menu"]`, `#mobile-menu`, `hidden`), never component internals, so they hold when the drawer is reimplemented - skip-link tests in `a11y.spec.ts`: first Tab stop, and Enter moving focus to `#main-content` - `smoke.spec.ts` now fails on `console.error`, not just uncaught exceptions, and asserts no duplicate `id` attributes per route Route lists moved to `e2e/routes.ts` so there is one copy, and `route-coverage.spec.ts` walks `dist/` and fails when a built route is in no list, when a listed route no longer exists, or when an adventure/level/solution route lacks axe coverage. That replaces the drift protection lost with the generated lists: a new adventure can no longer ship untested silently. The gate immediately found 12 level routes with no axe coverage. Added them, taking axe from 28 to 40 routes; the 24 remaining challenge-tag routes are listed as deliberate exclusions with a reason. Those 12 routes turned out to carry real contrast failures, fixed in the preceding commit. e2e goes from 244 to 371 tests, all passing. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
… script `eslint-plugin-jsx-a11y` went out with React and nothing replaced it, so no static accessibility rule had run against any `.astro` or `.vue` file since the migration. That is most of the site's markup. - Adds `eslint-plugin-vuejs-accessibility` (flat/recommended) and `eslint-plugin-astro`'s `flat/jsx-a11y-recommended` - Two rules tuned rather than silenced, both matching intentional patterns: `no-redundant-roles` allows `role="list"` on `<ul>`/`<ol>` (Safari VoiceOver drops list semantics when Tailwind removes list-style), carrying over the old config's exact allowance; `no-noninteractive-tabindex` allows `abbr` and `pre`, which must be focusable for the tooltip and for keyboard scrolling of code blocks. Everything else stays flagged - One targeted disable on the ChallengesFilter radiogroup, where the group is correctly non-focusable and the keydown handler catches arrow keys bubbling from the roving-tabindex radios Verified both rulesets fire: injecting an `<img>` with no alt into an `.astro` and a `.vue` file produces errors from each. Also adds `npm run check` (`astro check`) and `"types": ["vitest/globals"]` to tsconfig, which alone drops the reported errors from 256 to 57. It is NOT wired into CI yet: those 57 are pre-existing, and several sit in the components the island refactor is about to replace. Tracked in #217 with the breakdown and a suggested order. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…e lint disable
The arrow-key handler sat on the `role="radiogroup"` container and was reached
by bubbling, which meant an interactive handler on a deliberately non-focusable
element. That tripped `vuejs-accessibility/interactive-supports-focus`, and it
was suppressed with an inline disable rather than fixed.
Binding the handler to each radio removes the warning by making the code
correct, not by silencing the check:
- the handler now runs on the element that actually has focus when the key is
pressed, instead of relying on the event reaching a container that can never
be focused
- it keys off `e.currentTarget` rather than `document.activeElement`, so it
cannot act on the wrong radio
- sibling lookup uses `closest('[role="radiogroup"]')`, matching how
`navigatePanel` already resolves its own group
No inline eslint-disable comments remain anywhere in the repo.
This had no test in a real browser, which is how it could have broken silently.
Adds two to `challenges-filter.spec.ts`: arrow keys move focus, select, and wrap
in both directions; and exactly one radio is tabbable while the group stays out
of the tab order. Verified non-vacuous by removing the bindings and watching the
first fail.
The five existing unit tests dispatched `keydown` on the group, so they were
coupled to where the handler was bound rather than to the behaviour. Updated to
dispatch on the focused radio, as a real key event would. The
"no radio focused" test covered a branch that only existed with the old
binding; replaced with two that still mean something: non-arrow keys are
ignored, and the handler follows `currentTarget` rather than focus.
483 unit and 373 e2e tests pass.
Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>/challenges/<tag>/ rendered "<Tag> Challenges" as its h1 where the previous app
rendered "Open Source Challenges". More than a wording change: it made the same
filtered view show two different headings depending on how it was reached.
/challenges/ then click the Backstage pill -> "Open Source Challenges"
/challenges/backstage/ -> "Backstage Challenges"
Both render an identical grid of the same three challenges. The h1 names the
page; the filter is already conveyed by the active pill and the "3 challenges ·
Backstage" count line, so the heading changing was the odd one out.
The h1 is now unconditional. The <title> stays tag-specific
("Backstage Challenges - OffOn"), which was already at parity and is what
distinguishes these routes in search results and browser tabs.
This was the half of the review's F8 finding that the earlier P8 commit did not
cover: that one restored the titles and descriptions but left the heading.
Verified: h1 now matches the previous app on all 26 challenge routes, 0
mismatches. Adds 4 regression tests covering both ways of reaching a filtered
state, and asserting the tag is still surfaced in the visible count line and the
sr-only live region.
Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>…ghlighting Every Shiki error was caught and turned into `return null`, which the caller renders as a plain <pre><code>. So a broken highlighter setup failed nothing: build green, tests green, and syntax highlighting silently gone from the whole site. That is exactly what happened while fixing the comment-contrast colours, and the only reason it was noticed was an unrelated axe run. `return null` now means one thing only: the language has no bundled grammar, which is a content question and a legitimate quiet fallback. The four setup failures throw instead, each naming the stage and language and preserving the original error as `cause`: - highlighter initialisation - grammar load for a language that IS in bundledLanguages - codeToHtml rendering (the shape of the real incident) - output parsing, if Shiki's markup changes and no <code> can be extracted Chose failing the build over logging: these are build-time, so the e2e zero-console.error assertion would never see them, and a warning in build output is easy to miss. Adds `markdown-pipeline-shiki.test.ts`: 5 tests mocking each failure mode and 2 confirming the quiet path still works (unknown language renders plain, healthy Shiki still emits per-token colours). Verified every one of the 5 fails when its guard is removed. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
Removes the theme island. Both icons and both accessible names are rendered, and CSS picks between them off the `.dark` class on <html> using the same `hidden dark:block` technique as the Navbar logo. That fixes a real defect rather than just moving code. The island rendered the dark defaults server-side and only corrected them after hydration, so a returning light-mode visitor was served a sun icon and the accessible name "Switch to light mode" until the island booted. Both are now correct in the first painted frame with no JS at all. - The name comes from two sr-only spans, not aria-label, since an attribute cannot be swapped by CSS - One delegated click listener on document matched via `[data-theme-toggle]`. It survives View Transitions without rebinding and covers both instances - The `$theme` persistentAtom and its cross-instance `.listen` sync are gone. They only existed to reconcile two Vue copies of one control; both copies now read the same class, so there is nothing to reconcile. `stores/theme.ts` and `@nanostores/persistent` deleted, no other consumer Coverage moves from unit tests of the island's internals to 10 e2e tests of the behaviour, including two with JavaScript disabled that the previous implementation could not have passed: persistence across reload and client-side navigation, both instances agreeing across a breakpoint change, and the live region announcement. Docs updated in styleguide.md and CLAUDE.md, including the islands/hydration guidance that described the old SSR-mismatch workaround. 387 e2e and 462 unit tests pass. /about/ JS: 104.1 KB raw / 42.2 KB gz -> 100.5 / 40.4. Small so far because Vue still loads for ConsentBanner and MobileMenu; the drop lands once those two are ported. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
Ports the drawer to static markup plus one script. Every behaviour is preserved, and the 9 focus-trap tests written in Phase 3 pass unchanged: they were written against the DOM contract rather than the component, which is what made the swap verifiable rather than hopeful. Two things improved rather than just moved: - `aria-current="page"` is now resolved at build time. Navbar already computes `isActive()` for the desktop links and passes it through, so the drawer no longer re-derives the route from `window.location` after mount. The SSR markup is correct for assistive tech before any script runs, and the two navs cannot disagree - the focus trap uses the full 7-pattern focusable selector again (links, buttons, `input`, `select`, `textarea`, `[contenteditable]`, `[tabindex]`). The island had narrowed it to three, which was harmless only because the drawer holds links today Implementation notes worth knowing: - the trigger icon is chosen by CSS from `aria-expanded` via `group-aria-expanded:*`, so the icon cannot drift from the state assistive tech sees, and the script sets one attribute rather than swapping nodes - open styling is applied as classes by script, because Tailwind's `flex` sets `display:flex` and would defeat the `hidden` attribute if both were present - `transition:persist` is gone with the island, so the drawer is simply re-rendered per navigation. The script still releases on `astro:before-swap` to drop the keydown listener and any inert attributes Adds 3 tests to the existing spec: the icon tracks state, an injected `input` is inside the tab cycle (proving the widened selector), and crossing to the desktop breakpoint releases the trap. 390 e2e and 440 unit tests pass. /about/ JS now 95.5 KB raw / 38.0 KB gz, from 104.1 / 42.2 before Phase 4. Vue still loads for ConsentBanner. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
Ports the consent UI to static markup plus one script. Both states are rendered `hidden` and the script reveals whichever matches `$consent`. The state machine is untouched: src/stores/consent.ts still owns the storage format, the 180-day expiry, GPC handling and the gtag injection order, and its 54 unit tests are unchanged. All Phase 1 fixes preserved and now covered by e2e instead of Vue unit tests: the focus guard, the `max-h-[80vh] overflow-y-auto` reflow guard and safe-area padding, Decline-before-Accept order, and `.btn-secondary` styling. The focus guard gets simpler rather than reimplemented. The bug existed because a reactive watcher could not tell an init-time restore from a user action. Focus now moves only from the three click handlers, so there is no transition for it to misread; `initConsent()` cannot move focus by construction. Analytics lifecycle moved out of the component into its own script in Layout.astro. Binding `firePageView` in a component's mount hook meant a page_view could be missed if the island hydrated after `astro:page-load` had already fired. Verified at runtime: a returning granted visitor gets a page_view on first load, another on each client-side navigation, and undecided or declined visitors get none. Confirmed non-vacuous by delaying the listener registration and watching the first-load test fail. `@nanostores/vue` removed, no longer imported. `nanostores` stays: it is the state machine's storage and has full unit coverage. /about/ now renders zero islands. 404 e2e and 408 unit tests pass. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
Last of the four chrome islands. Rendered `hidden` and revealed by script only when `starter_nudge_dismissed` is absent, which also removes the flash the island had for returning visitors: it rendered nothing server-side and appeared after hydration regardless. Verifying it needed care. The nudge only renders when some adventure is live, and every deadline in the repo has now passed (latest 2026-07-28, today 2026-08-24), so it emits nothing on any page. The behavioural tests would have passed vacuously. So the spec has one test that always runs, pinning the rule that the nudge is present exactly when a live adventure exists, and the rest skip with a visible reason when there is none. To actually exercise the port I temporarily moved a deadline a year out, rebuilt and ran the suite: all 8 pass, covering reveal on both pages, the starter link target, dismissal persisting across a reload, the atomic live region and surviving a client-side navigation. Content reverted afterwards, no data change in this commit. Worth knowing separately: the nudge is currently invisible in production for that same reason. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…ive one The nudge picked the first *live* adventure, so once every deadline passed it rendered nothing at all. That is exactly when a newcomer still needs somewhere to start, and it is the state the site is in today: the newest deadline was 2026-07-28. Every page had a dead pointer. Not a migration regression, for the record: the React StarterNudge did `ADVENTURE_SUMMARIES.find((a) => a.isLive)` too. Same latent hole, just reached now that the dates have rolled past. Adds `getStarterTarget()` to src/lib/challenges.ts: the easiest level of the most recent adventure, with no live check. Challenges stay solvable after their rewards window closes, so "latest" is the useful answer. Two robustness choices, since the whole point is that this must not blank again: - sorts internally rather than trusting the caller's ordering - falls back to the easiest level present if an adventure ever ships without a Beginner, with unknown difficulties ranked last Also replaces the duplicated two-line starter lookup in index.astro and challenges/[...tag].astro with one call. Now renders on / and /challenges/, pointing at /adventures/dead-reckoning/levels/beginner/. 9 unit tests on the helper and 10 e2e, none skipped: previously 7 of them skipped because there was nothing live to exercise. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…udge
Rendered as "See ourPrivacy Policyfor details." and
"New here?Start with Dead Reckoning".
Astro strips the whitespace between a text node and an adjacent element when
the source has a newline there, so markup that reads correctly in the editor
loses the space in the output. Adds explicit `{" "}` at the three sites.
I introduced this in the Phase 4 ports, not the migration. Checked rather than
assumed: the React source carried explicit `{" "}` (JSX behaves the same way),
and Vue's compiler condensed the newline to a single space, which is why the
requirement disappeared from view. Both previous versions rendered correctly.
Adds `e2e/inline-spacing.spec.ts`, which scans the build for a link abutting a
word character on either side, allowing punctuation and brackets that
legitimately sit flush. It found exactly these three sites and nothing else
across all 69 pages. Verified it fails when the fix is removed.
Worth a guard rather than just a fix: this is a text-rendering defect that no
axe, smoke or a11y assertion looks at, and it is close to invisible when
reading the source.
Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>getStarterTarget() picked the most recent adventure outright. An open rewards window is the more useful destination, so live now wins when one exists. It stays a preference rather than a gate: with nothing live the pointer still resolves to the most recent adventure, which is the hole the previous live-gated version had. `find()` runs over the newest-first list, so several live adventures resolve to the newest of them rather than an arbitrary one. Adds 4 unit tests, including the divergent case the preference exists for: an older adventure live while a newer one has expired. Also covers several live at once, and a future *level* deadline counting as live rather than only a rewards deadline. Verified against a real build in both states by temporarily moving the oldest adventure's beginner deadline into the future: the nudge switched from dead-reckoning (newest, expired) to echoes-lost-in-orbit (oldest, live), and back on revert. No content change committed. Two test-only fixes found doing that: - the e2e helper detected liveness with `/\bLive\b/` against textContent, which cannot match inside "AdventureLive3 Levels", and it also matched non-card links. It now scopes to `a.card-glow` and looks for the pill element - adds `data-live-pill` to LivePill.astro as a stable hook, matching the data-attribute pattern used by the other ported components Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
"Start a Challenge" carries a down arrow but had been changed to navigate to
/challenges/, so the icon contradicted what the link did. Restored to
`href="#challenges"`, which is what the arrow describes and what the pre-migration
hero did.
Hero renders only on the home page, which still has `<section id="challenges">`,
and `html { scroll-padding-top: 80px }` keeps the target clear of the fixed
navbar. Drops the now-unused `base` binding.
Two tests: the CTA scrolls rather than navigating and lands the section below
the navbar, and the target exists on the page that renders the hero.
Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>Merges the spike. Every level card is rendered server-side with
data-difficulty / data-tags and filtering toggles `hidden`, replacing the Vue
island and its 24.7 KB of serialized props.
| route | JS before | JS after | HTML before | HTML after |
| --- | --- | --- | --- | --- |
| / | 42.1 KB gz | 9.8 | 25.9 KB gz | 20.7 |
| /challenges/ | 42.1 | 9.8 | 24.1 | 19.0 |
| /challenges/kubernetes/ | 42.1 | 9.8 | 20.9 | 19.1 |
HTML shrinks even on the tag route, which now carries 18 cards instead of 3:
losing the props JSON more than pays for cards that compress well against their
siblings. The build contains zero astro-island elements.
A tag route must carry every card, because the filter widens client-side
("All Tools", adding a second tag) without navigating. That is why the payload
could not simply be trimmed per route.
Two defects fixed in passing:
- the live region announced on load for a tag route, so arriving at
/challenges/kubernetes/ spoke "Showing 3 challenges · Kubernetes" with no user
action. Pre-existing Vue behaviour. `touched` is now set only by the action
handlers
- home passed the 6-capped preview count as `adventureCount` instead of the real
total, which would have been wrong the moment a 7th adventure landed
Coverage moves from 57 unit tests on the component to 22 e2e on the rendered
page, deliberately rather than incidentally. Ported: radiogroup structure and
labelling, aria-checked exact values and single-selection invariant, toggle-off,
All Levels clearing, tag aria-pressed both ways, non-arrow keys ignored, all
four URL-sync directions, results/adventures grid swapping, difficulty-only
matching, empty state, summary text, live-region attributes and timing, both
dropdown panels' role and label, opening one closing the other, mobile choice
reflected on desktop, `embedded` heading suppression, and the `seeAllHref`
condition.
Not ported, with reasons: "acts on the radio that received the key rather than
document.activeElement" tested a handler internal that no longer exists now the
listener is per-radio; "aria-checked is the string not just truthy" is subsumed
by toHaveAttribute; several near-duplicate structural assertions collapse into
one count; the remainder only exercised Vue prop plumbing.
ChallengesFilter.vue and its unit tests deleted. No .vue files remain.
Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>Framework policy, in styleguide.md, CONTRIBUTING.md and CLAUDE.md:
- Default to a `.astro` component with a plain `<script>`. Reach for a framework
only when the component has genuinely reactive state that a class toggle and a
small script cannot express.
- When one is warranted it is **Vue, never React**. `@astrojs/vue` and its
toolchain stay installed even though nothing currently uses them, so adding an
island is a one-file change. Called out explicitly so a dependency audit does
not strip them as unused.
- The five components removed from islands in Phase 4 are listed as the bar for
what does not justify a framework, with the challenge filter as the useful
reference: even two dropdowns, a roving-tabindex radiogroup, URL
reconciliation and a live region came out smaller as markup plus a script.
Also documents the Astro whitespace-stripping requirement, which was not written
down anywhere: inline links in prose need `{" "}` on both sides, JSX behaves the
same way, Vue's compiler did not, and `e2e/inline-spacing.spec.ts` is what
catches it.
CONTRIBUTING's code-style section still said "functional components with hooks
only, no class components" and "Tailwind on JSX", which had outlived React.
Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>`astro check` went 57 -> 74 with the Phase 4 and 5 ports. All 15 were mine. - TypeScript will not carry an `if (!el) return` narrowing into a hoisted function declaration, so every nested helper in MobileMenu and ChallengesFilter read its captured element as possibly null. Converted them to arrow consts, which narrow correctly - `hidden` is not a valid prop on the icon components; the two clear-icons now sit in a wrapping span that carries it - `isAdventureLive` and `getStarterTarget` declared `deadline?: string`, but the content collection renders it as `string | null | undefined`. Callers therefore failed to infer their own adventure type and lost `slug`, `title` and `tags`. Widened both, and `AdventureData` for the same reason Net 74 -> 44, so also below the 57 baseline recorded in #217. The new components report zero errors. The remainder is the pre-existing cluster tracked there. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
`astro check` reported 44 errors and had never run in CI, despite the repo
extending astro/tsconfigs/strict. Now zero, and wired into the `check` job in
preview.yml and deploy.yml next to sync and lint.
Six root causes, not 44 problems:
- `LUCIDE_ICONS` was typed `Record<string, Component>`. Vue's `Component` is a
union broad enough that the Astro checker cannot see an entry as renderable,
which surfaced as "X is not a valid component" at all five dynamic call sites.
Typed to the concrete unplugin-icons import instead
- `makeAdventure` in the challenges tests was inferred, so the literal widened
`difficulty` to string and the union with `overrides.levels` stopped matching
AdventureData. Annotated the return type: 19 errors
- the content collection renders `deadline` as `string | null | undefined`;
`RewardsCard` and the difficulty helpers declared `string`
- emoji lookup maps were indexed with a plain string
- `difficulty.test.ts` passes deliberately invalid values ("Novice",
"Unknown"); those are now explicit casts with a comment saying why, rather
than accidental `any`
- two `.astro` map callbacks needed their element type stated
No behaviour change: build, lint, 357 unit and 436 e2e tests unchanged.
Adds the type gate to the pre-submit checklist in CLAUDE.md, and fixes the
stale numbering and the "Vue components" reference in it (no .vue files remain).
Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>…216) The amber fill measured 1.59:1 against the near-white surfaces it sits on in light mode, well under the 3:1 WCAG 1.4.11 wants for the boundary of a control. Dark mode was never affected: the fill is 11.94:1 there. Took the recommended option from the issue, a light-mode border, with a darkened-amber `--primary-border` token rather than a neutral so it reads as part of the button rather than an outline round it. Measured on every surface the class actually appears on, not just the consent banner. A sweep of all `.btn-primary` instances across every route found three distinct backdrops in light mode: `--background`, `--card`, and the translucent consent banner over the page. The new border clears 3:1 against all of them: | | dark | light | | --- | --- | --- | | boundary vs surface | 11.94 (fill) | **5.13** (border), was 1.59 | | label vs fill | 11.94 | 12.67 | The border box is reserved in both themes with `border-transparent`, so nothing moves when the colour is applied. That also settles an existing inconsistency: `.btn-ghost` already carried a 1px border and rendered 46px while `.btn-primary` and `.btn-secondary` were 44px. All three now match. Adds `e2e/btn-primary-contrast.spec.ts`: for every route in both themes, every `.btn-primary` must clear 3:1 against its nearest opaque backdrop by fill or border, labels must clear 4.5:1, and the three button classes must stay the same height. 58 checks. 120/120 axe checks and 494 e2e still pass. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…test The vanilla filter deliberately stopped announcing on page load, but this assertion still required the sr-only live region to contain the count on a tag route. It has been failing since that change landed; I missed it because I was reading the tail of the Playwright output rather than its summary line, and the failure names sit directly under the "3 failed" header. The visible count line assertion is unchanged and still passes, so the tag was always still surfaced. The live-region assertion now checks the opposite and correct thing: empty on load. Announcement after an actual interaction is covered in challenges-filter-deep.spec.ts. Full suite: 497 passed, 0 failed. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
WAVE flagged very small text on the adventure pages. The only sub-10px text on the site was the avatar initials chip at `text-[0.6rem]` (9.6px), in AvatarLink.astro and CommunitySidebar.astro. Both now use `text-xs` (12px), the type scale minimum, which the repo's own styling rules ask for over arbitrary values. Verified the two-letter initials still fit the 24px circle with no overflow. The chips are not dead code, despite rendering `display:none` most of the time. They are the fallback for a failed avatar: the images are external Discourse URLs that go stale when a user changes their avatar, and the inline onerror swaps the image for the chip. All 132 current entries have an avatarUrl, so the chip is reached through that path rather than the no-avatar one. CommunitySidebar was only half-wired: its chip rendered solely in the no-avatarUrl branch, which never happens, while its <img> had no onerror at all. A failed avatar there showed a broken-image icon and the chip never appeared. Now matches AvatarLink. Keeps the per-index colour palette, which is why it does not simply reuse that component. Adds two guards: - `no very small text` in a11y.spec.ts, across all 40 routes, including hidden elements. Set at WAVE's 10px line rather than the 12px scale minimum because inline <code> renders at 11.9px by design - `avatar-fallback.spec.ts`, which blocks the avatar hosts and asserts no failed image stays displayed and a chip takes its place, on all three surfaces Both verified non-vacuous by reverting each fix in turn. Two things that made the fallback test subtle, noted in it: avatars are loading="lazy", so an image never scrolled into view is pending rather than failed and never fires onerror; and "displayed" is not the same as "broken", since an image that loaded fine is also displayed. 541 e2e, 357 unit, 0 astro check errors. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
- ConsentBanner.astro: rewrite script to astro:page-load / astro:before-swap lifecycle so Accept, Decline, and Reset work after client-side navigation; fixes Reset button stuck hidden for returning users after navigation - ChallengesFilter.astro: extract named handlers and add astro:before-swap teardown to stop mousedown/keydown listeners accumulating across navigations - solution.astro: move hashchange listener into page-load/before-swap lifecycle - content.config.ts: throw on adventure directory read failure instead of silently returning, preventing a silent empty-site deploy - eslint.config.js: add test-results/ to ignores to prevent ENOENT crash on fresh checkouts before any e2e run - CONTRIBUTING.md: fix dev-server port (8080 → 4321), test script name, and remove stale *.generated.ts claim - README.md: correct test:e2e description, remove Vue island references for ChallengesFilter, update nanostores pattern and stores listing - CLAUDE.md: update Stack section (zero islands, direct atom API), naming table, nanostores/vue install note, add mandatory listener lifecycle rule - styleguide.md: correct ChallengesFilter section (.astro + script, not .vue), fix filter pill reference, add teardown rule - AGENTS.md: full rewrite for Astro stack (was React Router v8 / React 19) - e2e/consent-ui.spec.ts: add 4 post-navigation regression tests - e2e/challenges-filter.spec.ts: add listener accumulation regression test Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
…ecycle - hash in URL opens the correct step on page load - no listener accumulation across client-side navigations; teardown fires on each astro:before-swap (mirrors the ChallengesFilter listener lifecycle test) Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
The absolute statement "must ... be torn down under astro:before-swap" was wrong for two existing components: - ThemeToggle.astro: module-scope delegated click on document, runs once per session, document survives all swaps, delegation matches at event time — no stale reference, no accumulation. - StarterNudge.astro: click listener on [data-starter-nudge-dismiss], a child of the component's own subtree. ClientRouter detaches the subtree on swap; dead listeners on detached nodes cannot fire and are GC-eligible. Next astro:page-load operates on fresh nodes. Updated CLAUDE.md and styleguide.md to name these patterns explicitly so a reader does not "fix" working components and introduce duplicate handlers. Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
Grep pass across all .md files for: react-router, dist/client, root.tsx, meta.ts, useEffect, useTheme, useConsent, .tsx/.jsx, generate-adventures, *.generated.ts, prerender array, public/sitemap.xml, lucide-react. Changes per file: - PERFORMANCE.md: update fonts section (root.tsx → Layout.astro, no route-level links() exports), JS section (React Router → Astro/Vite), markdown note (generate-adventures → content collection), script loading (remove RR8 claim), visibility section (useEffect → astro:page-load), prefetch section (SPECULATION_RULES/useEffect → Astro native prefetch), Cloudflare output dir (dist/client → dist/), new-routes checklist (remove react-router.config.ts/src/routes.ts/public/sitemap.xml) - ADVENTURES.md: sitemap is auto-generated; remove public/sitemap.xml from the hand-edit checklist - ACCESSIBILITY.md: lucide-react → unplugin-icons; Footer.tsx → Footer.astro - .claude/commands/progressive-enhancement.md: React Router v7 → Astro static output; dist/client → dist/ - .claude/commands/user-personalization.md: useTheme/useConsent hooks → ThemeToggle.astro + $consent nanostore - .claude/commands/add-solution.md: dist/client → dist/ - .claude/commands/create-presentation.md: react-router.config.ts/ src/routes.ts → Astro equivalents; *.generated.ts → adventure YAML Signed-off-by: Sinduri Guntupalli <sinduri.guntupalli@dynatrace.com>
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 freeto 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.
Summary
Completes the React Router v8 → Astro + Vue migration, then finishes the job the migration started: the site now ships zero framework islands. Every interactive surface is a
.astrocomponent plus a plain script.This branch also carries a full parity audit against the deployed React build, restoring content, SEO and accessibility guarantees that were silently lost in the migration, and closes#216 and #217.
Weight
Per-route, gzipped. "React" is the deployed pre-migration build on
gh-pages; "Vue islands" is the state this branch started in.//challenges//about/93% less JS than React on a content page. The framework was loading on all 69 pages to power a theme toggle, a drawer, a consent banner and a nudge. Those are now markup plus a script, and so is the challenge filter, which was the only component with a real case for reactivity.
astro-islandelements in the build: 0. Vue runtime chunks: 0.Parity verification against the deployed React build
idattributesastro checkThe one non-identical route
/adventures/lex-imperfecta/differs by a single character in its meta description: an em dash became a hyphen.This is intentional content drift, not migration drift.
CLAUDE.mdsays "Never use em dashes anywhere", and the change was made deliberately in744563f97("chore: exhaustive audit fixes across 20 files"). Zero em dashes remain in any adventure YAML. No action needed.What the parity audit found and fixed
Restored content and SEO that the migration had dropped:
BreadcrumbList,Course,LearningResource), which drive breadcrumb and course rich results. Now derived from the same crumb array that renders the visual breadcrumb, so the two cannot drift./handbook/, the only page that lost a whole content block./adventures/and/challenges/, which had lost their keyword phrases.click_eventpayload. All four parameter names had been renamed and one dropped, which would have silently killed every existing report at cutover.Accessibility fixes:
max-h-[80vh]reflow guard, so its actions could sit unreachable at 400% zoom.<abbr>on/handbook/was mouse-only.idattributes.Framework policy
Documented in
styleguide.md,CONTRIBUTING.mdandCLAUDE.md:.astro+ a plain<script>. Reach for a framework only when the component has genuinely reactive state that a class toggle and a small script cannot express.@astrojs/vuetoolchain stays installed even though nothing currently uses it, so adding an island is a one-file change. It is called out explicitly so a dependency audit does not strip it as unused.Also documents the Astro whitespace-stripping requirement (
{" "}around inline links), which was not written down anywhere and had already caused one visible bug.Issues resolved in this PR
Closes#216 —
.btn-primarymeasured 1.59:1 against the near-white surfaces it sits on in light mode, under the 3:1 WCAG 1.4.11 wants. Fixed with a darkened-amber border, measured against every surface the class appears on site-wide, not just the consent banner:The border box is reserved in both themes so nothing moves, which also settles an existing 2px height mismatch between
.btn-ghostand the other button classes.Closes#217 —
astro checkreported 44 errors and had never run in CI despite the repo extendingastro/tsconfigs/strict. Now 0 errors, and gated in both workflows. Six root causes, not 44 problems; the largest wasLUCIDE_ICONSbeing typed as Vue's broadComponentunion, which made every dynamic icon call site unrenderable to the checker.CI
The
checkjob now runssync,check(types),lintandtest:unit:coverage. Unit tests had not run in CI since the migration, and the coverage thresholds invitest.config.tshad never been enforced.Test coverage
Unit tests fall because the Vue component tests went with the components. That trade is deliberate: coverage moved to e2e against the rendered page, and this component's history is DOM-level bugs (focus-out, live region, heading, spacing) that unit tests did not catch. The meaningful edge cases from the 57
ChallengesFilterunit tests were ported; the ones left behind only exercised Vue prop plumbing or handler internals that no longer exist.New guards worth noting: a 750 KB page-weight budget with a third-party allowlist, a route-coverage drift gate that fails when a new route is untested, a duplicate-
idcheck per route, an inline-spacing scan, andconsole.errorfailing the smoke suite.On the e2e counts moving around in this branch's history: during the filter work, three e2e failures were reported as passing. Playwright prints failure names beneath its
N failedheader, and reading the truncated tail of the output made them look like the last passing tests. The failures were real but benign — a stale assertion requiring the live region to announce on page load, which is exactly the behaviour that change deliberately removed; the visible count-line assertion beside it passed throughout, so nothing was ever missing from the page. Caught and fixed in0d213b393. Result counts are now read from the summary line, not the tail. Final: 497 passed, 0 failed.Follow-ups, not blocking
astro:assetsusage anywhere;public/team,public/speakersandpublic/solutionsare the obvious<Image>candidates.AGENTS.mdstill describes the React/RR7 architecture in places.