Skip to content

Astro 5 → 6 migration#4

Merged
larsweiser merged 14 commits into
mainfrom
dev
Apr 28, 2026
Merged

Astro 5 → 6 migration#4
larsweiser merged 14 commits into
mainfrom
dev

Conversation

@larsweiser
Copy link
Copy Markdown
Collaborator

@larsweiser larsweiser commented Apr 28, 2026

Summary

Astro 5 → 6 migration. Zero behavioural change; build emits identical static output. End-state: dev.blackbrowedlabs.com and blackbrowedlabs.com both serve identical HTML/CSS/JS to pre-migration, but built by Astro 6 / Vite 7 / Zod 4.

Sub-step diffs (atomic single commit):

  • package.json — astro ^5.2.0^6.1.9; @astrojs/check ^0.9.4^0.9.8; new overrides: { vite: "^7" } per Astro PR #16062 (canonical fix for the Vite 7/8 hoisting collision with @tailwindcss/vite@4.2.x)
  • src/content.config.ts — Zod-4 schema deltas (z.string().url()z.url() ×2); z import moved to zod direct (clears the soft-deprecation hints)

Workspace docs (gitignored under plans/) refreshed in the same session — Wave-1 A5 inventory swept end-to-end (~9 actionable rewrites + ~21 verified-no-edit rows). Not in this diff because plans/ is gitignored per docs/TECH_STACK.md §8.3.

Backed by seven audit reports under plans/active/pass-2/astro-6-migration/:
A1 code readiness · A2 tech-stack ripple · A3 Cloudflare/Wrangler · A4 Zod 4 · A5 docs inventory · A8 / A9 / A10 plan coverage / executability / freshness.

Mid-flight diagnostic: vite override added after Tailwind 4.2.x hoisted Vite 8 broke the local build with Missing field tsconfigPaths on BindingViteResolvePluginConfig.resolveOptions. Canonical fix per Astro maintainers (PR #16062) shipped in Astro 6.1; we missed the codemod by bumping deps manually.

Test plan

  • npm run check — 0 errors, 0 warnings, 1 pre-existing hint
  • npm run build — 4 pages built; dist/ shape unchanged
  • Staging deploy green (45 s)
  • Staging curl: DE/EN home + about lang attrs correct; x-robots-tag: noindex, nofollow intact
  • Staging visual smoke (DE/EN home + about, theme + lang toggles, mobile 360, Lighthouse parity)
  • build-pr CI green on this PR
  • Production smoke after merge (same checklist on blackbrowedlabs.com)
  • CWA beacon still absent from production HTML (until Phase B.3 ships)

🤖 Generated with Claude Code

blackbrowed-labs and others added 14 commits April 24, 2026 09:17
Adds §8.4 covering:
- Repository visibility: public as of 2026-04-23, post secret scan
- Pre-flip secret scan procedure (gitleaks)
- Branch protection rulesets: protect-main and protect-dev
- Rationale for deferring the required-status-checks toggle to Pass 2

Also trims §2.1's visibility bullet to cross-reference §8.4 instead of
duplicating the pre-flip scan procedure.

Also adds Pass 2 backlog item #5 for adding a PR-triggered CI workflow
that can serve as the required status check once in place (backlog file
itself is under plans/ and gitignored — this commit is docs-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SiteHeader.astro: change .site-header from position: relative to
  position: sticky; top: 0; z-index: 50. Keeps the hairline bottom
  border. Background is already opaque (var(--color-bg)) — header will
  not show through to scrolled content.
- global.css: add scroll-padding-top: 5rem to html so the "Skip to
  main content" link's target (#main) lands below the sticky header
  instead of behind it. Also covers any future anchor jumps.

No JS, no scroll-driven effects, no frosted/translucent surface —
plain position: sticky per CLAUDE_DESIGN_BRIEF §8 (paper before screen,
no ornament). No animation, so prefers-reduced-motion is unaffected.

Z-index 50 sits above the in-header mobile-nav panel (10) and
ThemeToggle menu (20), well below the SkipLink (1000) so the skip
link stays visible on focus.

sticky also qualifies as a "positioned" value, so the mobile nav
panel's `position: absolute; top: 100%` still anchors to the header
correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standard mobile modal pattern: when the hamburger menu is open, body
scrolling is disabled via `body.menu-open { overflow: hidden }` gated
to mobile breakpoints only. Preserves scroll position on close
(no position: fixed reset).

Includes focus management (move to first nav item on open, return to
hamburger on close via Escape or tap-outside), focus trap within the
menu panel (Tab / Shift+Tab cycle), aria-expanded toggling on the
hamburger button, and auto-close if the viewport resizes to desktop
while the menu is still open. Honors prefers-reduced-motion (the menu
has no transitions, so nothing to disable beyond the existing global
@media gate in global.css).

Desktop nav unaffected — always visible, never locks body scroll.

Smoke-tested via plans/diagnose-mobile-menu.mjs (Playwright, 360px
viewport): 12/12 checks pass covering class toggle, aria-expanded,
body overflow state, nav data-open, focus placement, scroll
preservation, Escape/focus-restore, focus trap, resize auto-close,
and desktop no-op gating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new sections between "Implementation phasing" and "Skills":

- "Gate-based handoff pattern" — four rules on how to sequence work
  with Claude: plan-first for multi-decision tasks, pause at named
  gates, report diagnostics before editing, surface uncertainty as
  open questions rather than silent guesses. The discipline Pass 1
  shipped on.
- "Process rules from Pass 1" — three specific technical lessons
  extracted from bugs we caught during G1b / G4 / post-deploy:
  token-conversion verification scope (diff both preset and
  companion CSS), wrangler env-inheritance gotcha (observability /
  vars / routes don't inherit into env.* blocks), and primary-
  source verification for version-dependent API behavior.

Closes items #2, #3 of the Pass 2 backlog (which also had them
scheduled for a CLAUDE.md touch at Pass 2 start; landing them now
keeps the rules in the file when the next session picks up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0.1 — TECH_STACK.md §3.6 (new subsection under Hosting —
Cloudflare Workers). Documents that Cloudflare's zone-level "Manage
robots.txt" is deliberately enabled:

- Production: the managed block names known AI-training crawlers
  and Cloudflare maintains the list over time (no manual
  maintenance).
- Staging: the managed block prepends User-agent: * / Allow: /
  ahead of our Disallow: /, which a crawler may read as an
  override; the X-Robots-Tag header and <meta name="robots">
  (§3.3) remain the authoritative noindex signals.
- Toggle at Cloudflare dashboard → Bots / Scrape Shield.

Phase 0.2 Part 1 — IHK Hamburg → IHK zu Lübeck correction in the
two real occurrences (verified via pre-write grep; the plan's
three-places claim was reconciled on-disk in the v2.1 changelog
note):

- docs/BASELINE_COPY.md:222 — §9 "Disclaimer for the owner" callout
- docs/TECH_STACK.md:648 — §11.2 Datenschutzerklärung disclaimer

Reinbek is in Kreis Stormarn, Schleswig-Holstein. IHK zu Lübeck
covers Ostholstein, Segeberg, Stormarn, Herzogtum Lauenburg, and
Hansestadt Lübeck, with an Ahrensburg Geschäftsstelle ~9 km from
the registered address — relevant for the planned post-launch IHK
consultation (per OQ-8 resolution in the Pass 2 plan).

Pass 2 Phase 0 Commit A (of three). §9.8 / §10.8 CWA carve-out
(Commit B) awaits drafted text from the fresh reviewer session.
Phase 0.3 CI workflow follows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0.2 Part 2 — amend the "no external scripts" clause in §9.8
(DE, legally authoritative) and §10.8 (EN, courtesy translation)
so the disclosure tracks reality once the CWA beacon lands in
Phase B.3.

Replacement text drafted in a fresh Claude.ai reviewer session per
the §11.4 standing rule (Pass 2 plan). Lars-approved verbatim;
Claude Code implements without edits. DE-EN register asymmetry
preserved — DE closes with "vom eigenen Server" (stronger), EN
closes with "site's own origin" (softer), matching the original
text's asymmetry.

Phase 0 Commit B (of three). Phase 0.3 CI workflow is next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0.3 sub-step 2 — author the PR-triggered CI workflow that will
become a required status check on the protect-main ruleset
(docs/TECH_STACK.md §8.4).

Workflow shape:
- Trigger: pull_request → main
- Node 24 + npm cache (matches deploy-staging.yml exactly)
- Steps: npm ci → npm run check → npm run build
- Job key + display name both `build-pr` so the ruleset UI's
  required-check dropdown surfaces a single unambiguous match
- Concurrency: per-PR with cancel-in-progress: true (different from
  deploys, which serialise) — fresh pushes cancel in-flight checks
- permissions: contents: read (minimum needed)

Forward-defense: workflow-level env FORCE_JAVASCRIPT_ACTIONS_TO_NODE24
opts into Node 24 for JS-based actions ahead of GitHub's runtime
default flip on 2026-06-02. Per the Pass 2 backlog Node 20
deprecation item — the existing deploy-* workflows stay untouched
until that backlog item is picked up properly.

Pre-flight verified before authoring: package.json already has
scripts.check = "astro check" and @astrojs/check in devDeps, so no
package.json edit lands here. Sub-step 1 of Phase 0.3 is a no-op.

Phase 0 Commit C (of three on dev). Sub-steps 3–5 of Phase 0.3
(throwaway PR to register the check, ruleset update, end-to-end
verify) follow as session actions, not commits. G1 closes Phase 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace `export type UiStrings = typeof de` with a recursive
WidenStrings<T> helper that preserves the nested object shape but
widens every string leaf to `string`. Resolves 12 ts2322 errors in
en.ts where English literals (e.g. 'About') were rejected against
German literal types ('Über').

Type-only change; runtime values are unchanged. Verified via
`npm run check` (12 cluster-B errors gone, 19 cluster-A errors
remaining as expected) and against the A.0 consolidator's worktree.

Phase A.0 (1 of 2) — closes part of Pass 2 backlog §7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SiteHeader.astro:
- Re-bind `trigger` and `nav` to triggerEl / navEl typed locals after
  the existing L67 `instanceof HTMLElement` guard so the narrowing
  propagates into every inner-function closure
- Apply the rename consistently across the IIFE, including spillover
  sites at L106, L116, L138 that aren't in the original error list
  but would otherwise produce new ts18047 errors after the rebind
- Annotate `close(restoreFocus: boolean)`
- Use `navEl.querySelectorAll<HTMLElement>(selector)` so getFocusables()
  returns HTMLElement[], unblocking .focus() and .offsetWidth/Height

ThemeToggle.astro:
- Add a local `type ThemeChoice = 'light' | 'dark' | 'system'` alias
- Annotate `resolve`, `apply`, `persist` with `choice: ThemeChoice`;
  add explicit return types on `readChoice()` and `resolve()`
- Use `querySelectorAll<HTMLElement>(...)` for triggers/menus/options
  so .hidden IDL access works
- Add a boundary-validation guard in the option-click handler that
  narrows the data-theme-value attribute string to ThemeChoice
  before passing to persist/apply

Type-only changes — no runtime behavior change. Pass 1 G4 walkthrough
behaviors (mobile menu open/close, focus trap on Tab/Shift+Tab,
body-scroll-lock, focus restoration, ARIA states, theme apply/persist/
resolve flow) preserved. Verified via `npm run check`
(0 errors / 0 warnings / 1 hint on the read-only handoff bundle) and
`npm run build` (4 pages, 355 ms, no warnings).

Phase A.0 (2 of 2) — closes Pass 2 backlog §7 fix portion.
Bootstrap-PR re-run + protect-main ruleset flip follow as session
actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase A of Pass 2: shared foundations. Adds Pass 2 routes to the
counterpart map; adds nav + footer strings for the upcoming pages;
declares editorial / products / releases collections; migrates
Home + About copy from inline literals to Markdown.

The editorial schema gains intro[] and closing fields to preserve
the asymmetric .about__top grid and styled .about__closing footer
without breaking visual parity. Smartypants disabled in markdown
config so BASELINE_COPY apostrophes survive verbatim.

No visible change on staging. Phase B (pages) consumes these
foundations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nine findings drafted in the auditor/drafter-separated review cycle and
approved verbatim by Lars. Affects Impressum (§7/§8) and Datenschutz
(§9/§10). Introduces three Phase D build-time placeholders for
Cloudflare-published facts. Stream C (§6 contact form) deferred to
Phase B.1. See plans/g2-5-legal-amendment-drafts.md for the full
token inventory and per-finding rationale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 17-line ## Workspace bootstrap section between Priority reading
and Core principles. Points fresh Claude Code sessions at plans/BOOTSTRAP.md
→ plans/INDEX.md → plans/HANDOFFS/ for orientation. Documents the three
orchestrator standing rules: per-occurrence propose-then-validate INDEX
maintenance (never hand-edited), Opus default for sub-agent dispatch
(Sonnet only for mechanical tasks with frontmatter justification), and the
co-located prompt-file convention at plans/active/<topic>/prompts/.

The supporting workspace structure under plans/ is gitignored — this
commit only persists the CLAUDE.md entry point. Workspace design,
implementation, and verification are all complete (A6 v1-v4, A6b, A6c).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- package.json: astro ^5.2.0 → ^6.1.9, @astrojs/check ^0.9.4 → ^0.9.8,
  add overrides: { vite: "^7" } per Astro PR #16062 (fixes Vite 7/8
  hoisting collision with @tailwindcss/vite@4.2.x)
- src/content.config.ts: split z import to zod direct; migrate
  z.string().url() → z.url() (×2) for Zod 4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reconciles the squash-merge commit d00d6f1 (Pass 1 follow-ups PR #1)
that landed on main on 2026-04-24 with the original commits on dev.
Conflict in src/components/SiteHeader.astro resolved by keeping dev's
typed-locals form from 78c0202 (TS strict-mode fix) — main's squash
predates that fix.

Pre-step for the natural Pass 2 dev → main release PR (#4) so the merge
becomes a clean fast-forward instead of carrying a conflict into review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@larsweiser larsweiser merged commit eb5692f into main Apr 28, 2026
2 checks passed
blackbrowed-labs added a commit that referenced this pull request Apr 28, 2026
Same pattern as commit 7d83c4b: PR #4 was rebase-merged onto main on
2026-04-28, replaying dev's 13 commits with new SHAs. dev's tree
already matches main content-wise via the earlier merge 7d83c4b, but
git sees the histories as divergent. Conflict in src/lib/i18n.ts
resolved by keeping HEAD (the /404 ↔ /en/404 counterparts added in
89129ce — main doesn't yet have them).

This unblocks PR #5 (Phase 404 → main).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
blackbrowed-labs added a commit that referenced this pull request Apr 28, 2026
Initially set to `enabled` at Pass 1 branch-protection config. Three
sequential dev→main PRs (Phase M #4, Phase 404 #5, Phase B.1 #6)
each tripped on the same friction: GitHub's rebase-merge rewrites
SHAs, leaving dev's history "stale" against main's new SHAs. Each
time required a manual realignment — temp-disable protect-dev's
force-push block, reset dev to main, force-push, re-enable. Three
toggle dances in two days.

Allowing merge commits on main removes the rewrite, preserving
original SHAs across both branches. Future dev→main PRs use
"Create a merge commit" (now the GitHub default since linear
history is off). dev and main never drift.

Trade-off: main's git log gains one merge commit per release.
`git log --first-parent main` gives the linear release-only view.
Squash and rebase merges remain available but are not the default.

Documents the change in §8.4 with the full reasoning chain so the
next contributor (or future Lars) understands why merge commits
sit alongside otherwise-strict main protection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
blackbrowed-labs added a commit that referenced this pull request Apr 29, 2026
…ed pages

Closes the breadcrumb i18n retrofit gap surfaced by both G C.2.b reviewers:
my spec said "all 8 breadcrumb-bearing pages" but the site actually has 12.
Four pages were left out and continued to hardcode aria-label="Breadcrumb",
including two DE pages (ueber, kontakt) that consequently served the English
label to German screen-reader users — the locale-mismatch bug fold-in #4
was meant to eliminate.

Pages retrofitted:
- src/pages/ueber.astro              (DE → Brotkrümelnavigation)
- src/pages/kontakt.astro            (DE → Brotkrümelnavigation)
- src/pages/en/about.astro           (EN → Breadcrumb, now i18n-driven)
- src/pages/en/contact.astro         (EN → Breadcrumb, now i18n-driven)

Each page gains:
- Import: getUiStrings from the appropriate relative path
- Const: t = getUiStrings('de'|'en')
- aria-label="Breadcrumb" → aria-label={t.breadcrumb.ariaLabel}

No other changes; no content/whitespace/CSS drift on these pages.

Verification:
- npm run check: 0 errors.
- npm run build: 15 pages.
- All 12 breadcrumb-bearing pages now serve their locale-correct
  aria-label: 6 DE pages ("Brotkrümelnavigation"), 6 EN pages
  ("Breadcrumb"). Zero pages still hardcode "Breadcrumb" in DE markup.
blackbrowed-labs added a commit that referenced this pull request Apr 29, 2026
…failures" design

Adds verify-cloudflare-facts.yml weekly cron workflow + scripts/checks/dpf.mjs
+ scripts/checks/cwa-retention.mjs + 6 fixtures + scripts/run-verifier.mjs
orchestrator. Extends scripts/check-cloudflare-facts-freshness.mjs to read
_meta.last_check_attempt with 30-day threshold (down from 90).

Migrates src/data/cloudflare-facts.json to verifier-era schema (schema_version
1; per-fact status + value fields; _meta block; structured cwa_retention with
integer month values; raw_events_retention_months explicitly null per
Cloudflare's documented absence). Updates src/lib/cloudflare-facts.ts type +
adds getEffectiveVerifiedDate helper (worst-case freshness signal: older of
the two per-fact verified_at). Privacy pages (datenschutz.astro, en/privacy.astro)
read via the helper.

Verifier shape per plans/active/pass-2/g-d-2/spec.md: 1/5/15-min retry budget
(21 min total, fits 30-min workflow timeout); status-return error handling
(no throws cross check boundaries); explicit registry per spec §9.6;
hand-written validator per §9.7; status-enum rename ('ok' CheckResult →
'active' JSON) per §6.2; v1-coverage smoke step inside verifier workflow only,
mode #8 deploy-triggered limitation acknowledged per §3.8.

All synthetic failure modes (#2, #4, #5, #6) verified via MOCK_SCENARIO=
fixture routing. Mode #1 (freshness gate) verified via stale last_check_attempt.
Modes #3, #7, #8, #9 deferred to controller-side post-commit per prompt's
"may defer" guidance. Outcome record at
plans/active/pass-2/g-d-7/verifier-test-matrix.md (workspace, gitignored).

Closes backlog #8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants