Skip to content

Migrate to Astro 7, TypeScript 7, and Node 24 LTS#51

Merged
plx merged 4 commits into
mainfrom
plx/upgrade-astro-typescript
Jul 23, 2026
Merged

Migrate to Astro 7, TypeScript 7, and Node 24 LTS#51
plx merged 4 commits into
mainfrom
plx/upgrade-astro-typescript

Conversation

@plx

@plx plx commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary

Upgrades the site to Astro 7, adopts the TypeScript 7 native compiler for type-checking, and refreshes the Node pin and build toolchain — with a verified-identical rendered result (all 24 routes byte-for-byte equivalent in structure; pixel-parity across a full Playwright visual-regression matrix).

Version changes

Package Before After Notes
astro 5.18.1 7.1.3 two majors
@astrojs/mdx 4.3 7.0
@astrojs/react 4.4 6.0
@astrojs/rss / sitemap 4.0 / 3.7 4.0 / 3.7 patch
astro-expressive-code 0.41 0.44
astro-seo 0.8 1.1 output verified identical
@astrojs/tailwind 5.1 removed incompatible with Astro 7
tailwindcss 3.4 3.4 kept; wired via PostCSS
typescript 6.0 6.0 kept for API-dependent tooling (see below)
@typescript/native-preview (tsgo) 7.0 the TypeScript 7 compiler
accessible-astro-components 4.1 removed unused
Node (.nvmrc) 24.15.0 24.18.0 active LTS

Key decisions & things discovered

TypeScript 7 is the native compiler, and the ecosystem hasn't caught up. typescript@7 no longer ships the JS programmatic API that astro check (via @astrojs/language-server) and typescript-eslint depend on — both fail hard on it (astro check errors; typescript-eslint has an explicit "does not support TS 7.0" guard). Following the TS team's own side-by-side guidance, the project:

  • type-checks with the TS7 native compiler (tsgo)npm run typecheck, now also a CI step;
  • keeps classic typescript@6 so astro check and ESLint keep working (no overrides needed).

This is a faithful TS7 adoption: the authoritative type-check runs on TS7. Remove typescript@6 once those tools support the native compiler.

@astrojs/tailwind doesn't support Astro 7. Replaced it with a postcss.config.mjs running tailwindcss v3 + autoprefixer — the exact pipeline that integration ran. Generated CSS is functionally identical (only newer-minifier reformatting: property reordering, value normalization like flex:1 1 0%flex:1, and rule-grouping). Tailwind v4 was deliberately deferred — its Preflight/default changes (e.g. backdrop-blur-sm 4px→8px, default border color) risk visual regressions this migration is meant to avoid. Confirmed the header blur stays 4px and default border stays var(--border).

Content collections migrated to the Content Layer API. The legacy type: "content" API is gone in Astro 7; collections now use glob() loaders in src/content.config.ts. A custom generateId strips /index so folder-based posts/projects keep their bare slugs — every existing URL is preserved (verified: identical 24-route manifest). Consumers updated from .slug/entry.render() to .id/render(entry).

Astro 7 trims inter-element whitespace that Astro 5 preserved. The only visible consequence was tighter spacing around the · separator in home-feed meta rows; restored with explicit {" "} in ExcerptEntry (measured back to pixel-identical). This also removes the design's fragile reliance on incidental template whitespace.

Deprecations: adopted import { z } from "astro/zod" and z.url() (Astro/Zod-v4 replacements) — astro check is now fully clean (0 errors / 0 warnings / 0 hints). Left markdown.rehypePlugins in place (still supported; migrating to the unified() processor risks changing GFM/markdown output) — noted as a follow-up.

Verification

  • Full CI suite green: lint, tsgo typecheck, 22 unit tests, spellcheck (source + HTML), Vale prose + fixtures, build (astro check: 0/0/0), internal-link validation, RSS feed validation.
  • Route parity: identical set of 24 HTML routes + rss.xml / sitemap-index.xml / robots.txt.
  • HTML diff (normalized): only non-user-facing deltas — whitespace trimming, "" entity style, and inline-script minification.
  • Visual regression: Playwright screenshots across 11 representative pages × {desktop, mobile} × {light, dark} vs. the pre-migration build — effectively pixel-identical (worst residual is text antialiasing / a 1px total-height rounding on a 22k-px page; content confirmed identical).

Follow-ups (out of scope)

  • Tailwind v3 → v4 (needs its own visual-regression pass).
  • markdown.rehypePluginsmarkdown.processor: unified(...).
  • Keep the dated @typescript/native-preview snapshot and lockfile entry refreshed until TypeScript 7 is stable; drop typescript@6 once astro check / typescript-eslint support the native compiler.

🤖 Generated with Claude Code

Upgrade the framework, type-checker, and build toolchain with a
verified-identical rendered result.

- Astro 5 → 7.1.3; integrations bumped (mdx 7, react 6, rss, sitemap,
  expressive-code 0.44, astro-seo 1.1).
- Content collections migrated from the legacy `type: "content"` API to
  Content Layer `glob()` loaders (src/content.config.ts); a custom
  generateId strips `/index` so every existing URL is preserved.
  Consumers updated for `.id` and `render()`; `z` now from `astro/zod`
  and `z.url()`.
- Tailwind: replaced the Astro-7-incompatible @astrojs/tailwind
  integration with a PostCSS pipeline (tailwindcss v3 + autoprefixer),
  producing functionally identical CSS. Tailwind v4 deliberately
  deferred to avoid visual regressions.
- TypeScript 6 → 7 via @typescript/native-preview (tsgo) for
  type-checking. Classic typescript@6 kept for `astro check` and
  typescript-eslint, which don't yet support the native compiler.
- Node pinned to 24.18.0 (active LTS); added a TS7 typecheck step to CI.
- Restored explicit spacing around the ExcerptEntry meta separator
  (Astro 7 trims inter-element whitespace).
- Dropped the unused accessible-astro-components dependency.

Verified: all 24 routes identical; full CI suite green; Playwright
visual regression across 11 pages x desktop/mobile x light/dark shows
pixel-parity with the pre-migration build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review

Solid, well-documented upgrade — the PR description is unusually thorough (version table, rationale per decision, verification methodology). Went through the non-lockfile diff in detail; overall this looks safe to merge. A few notes:

Content Layer migration (src/content.config.ts)

  • The stripIndexId custom generateId is applied to blog and projects (folder-based, <slug>/index.md) but not to briefs (flat <category>/<name>.md), which relies on the Content Layer's default id-generation. That's fine functionally today since all brief filenames are already lowercase/kebab-case, but it's worth double-checking: does Astro 7's default glob-loader generateId slugify the same way the legacy type: "content" API did (e.g., for uppercase letters, spaces, or non-ASCII filenames)? If a future brief filename isn't already slug-safe, its URL could silently differ from what the legacy API would have produced. Since the PR already did rigorous route-parity verification, this is probably covered, but it's an asymmetry that might be worth a one-line comment explaining why briefs doesn't need the override (or applying stripIndexId-style consistency there too, for defense against future filenames).
  • ogFields.ogImage moved from z.string().url() to z.url() — correct per the stated Zod v4 migration and consistent with demoURL/repoURL in the projects schema.

@typescript/native-preview pin

  • Pinned to ^7.0.0-dev.20260707.2, a dated nightly of the unreleased TS7 native compiler. That's reasonable given TS7 isn't GA yet, but dev/nightly package versions are sometimes unpublished or superseded faster than stable releases — worth keeping an eye on whether this needs periodic bumping to avoid npm ci failures if the exact dev snapshot gets pulled from the registry. Might be worth a tracking note alongside the existing "remove typescript@6" follow-up.

readingTime(post.body ?? "")

  • Reasonable defensive change given CollectionEntry.body is now optional under the Content Layer API types (src/pages/blog/[...slug].astro, src/pages/projects/[...slug].astro). No behavior change for real entries since body is always populated by the glob loader.

ExcerptEntry.astro whitespace fix

  • The {" "} insertions to compensate for Astro 7's inter-element whitespace trimming look correct and are scoped tightly to the · separator — good catch, and nice that it's called out as removing a "fragile reliance on incidental template whitespace" rather than just patched over.

CI (build.yml)

  • New Type Check (TypeScript 7) step placed right after lint and before the build step (which itself runs astro check via npm run build) — good ordering, fails fast on type errors from the more authoritative TS7 checker before the slower build/spellcheck/Vale steps run.

Nit

  • postcss.config.mjs is a good, minimal replacement for the removed @astrojs/tailwind integration, and the comment explaining why it exists (not just what) is exactly the kind of comment that earns its keep.

Security / test coverage

  • No security concerns — this is a pure toolchain/dependency upgrade with no new external inputs, API surface, or data handling changes.
  • No new automated test coverage was added for the migration itself, but given the extensive manual verification described (route parity, HTML diffing, Playwright visual regression across viewport/theme combinations), that seems like an appropriate substitute for a change of this shape — it'd be hard to unit-test "the rendered output is unchanged" better than what's already described.

Nice work — the discipline around verifying byte-for-byte/pixel parity before landing a two-major-version framework bump is exactly the right way to de-risk this kind of change.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

PR Validation ✅

All checks passed! Ready for review.

Checks Performed:

✓ Linting
✓ Unit tests
✓ Spell check (source)
✓ Prose lint (Vale)
✓ Vale fixture tests
✓ Type checking & Build
✓ Spell check (HTML)
✓ Internal link validation
✓ RSS feed validation
✓ Artifact upload

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@github-actions

Copy link
Copy Markdown
Contributor

PR Validation ✅

All checks passed! Ready for review.

Checks Performed:

✓ Linting
✓ Unit tests
✓ Spell check (source)
✓ Prose lint (Vale)
✓ Vale fixture tests
✓ Type checking & Build
✓ Spell check (HTML)
✓ Internal link validation
✓ RSS feed validation
✓ Artifact upload

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review

Solid migration — the write-up is thorough and the verification approach (route parity + Playwright visual regression) is exactly right for a "no visible change" refactor like this. Went through the non-lockfile diff in detail; a few notes, nothing blocking.

Code quality / correctness

  • src/content.config.ts stripIndexId (entry.replace(/\/index\.mdx?$/, "").replace(/\.mdx?$/, "")): the second .replace() is a no-op for the documented folder-based layout (<slug>/index.md) since the first regex already consumes the extension whenever the first branch matches. It only bites if someone ever drops a bare my-post.md straight into src/content/blog/ (bypassing the folder convention) — in that case it strips the extension too. Given CLAUDE.md documents the folder convention as required, this is a harmless defensive extra rather than dead code, but a one-line comment noting it's a fallback for non-folder entries would save the next reader a double-take.
  • post.body ?? "" in [...slug].astro for blog/projects: reasonable guard now that body is typed as possibly-undefined under the Content Layer API. Since verification showed byte-identical output across all 24 routes this is presumably already covered, but worth keeping an eye on that readingTime never silently degrades to "0 min read" for a real post.
  • briefs collection intentionally keeps the default generateId while blog/projects use stripIndexId — the asymmetry is explained inline, good call documenting why rather than just doing it.
  • Good job tracing .slug.id and .render()render() through every consumer (contentCardHelpers.ts, rss.xml.ts, all three [...slug].astro pages, [category].astro, briefs/index.astro). I checked for stragglers and only found metadata.slug in briefs/index.astro:90, which is unrelated (category metadata, not a CollectionEntry) — not a bug.

Dependency/toolchain

  • Confirmed in package-lock.json that removing the @astrojs/checktypescript override is safe: there's a single resolved typescript@6.0.3 in the tree, and @astrojs/check@0.9.9's peer range (^5.0.0 || ^6.0.0) is satisfied without it. No duplicate/conflicting TS install.
  • @typescript/native-preview is pinned to a dated dev snapshot (^7.0.0-dev.20260707.2). Using ^ on a prerelease version is unusual — semver caret ranges on prereleases only match that exact prerelease line, so this is effectively a pin, which is presumably intentional (matches the PR's own "keep the dated snapshot refreshed" follow-up note). Just flagging in case the ^ was meant to allow patch-level dev snapshot bumps, which it won't actually do.
  • The dual-TypeScript setup (tsgo for the real typecheck, classic typescript@6 kept alive only for astro check/typescript-eslint) is well-reasoned and clearly called out as temporary, with a tracked follow-up to drop typescript@6. Good discipline not letting this linger silently.
  • Minor/pre-existing: autoprefixer/postcss (and other tool packages like eslint, tailwindcss, typescript) land under dependencies rather than devDependencies. Not a regression this PR introduces (the project already mixes build tooling into dependencies), and it doesn't matter functionally since npm ci installs everything regardless — just noting for anyone doing a future dependency-hygiene pass.

Design-system / rendering

  • The ExcerptEntry.astro fix for Astro 7's stricter whitespace trimming ({" "} around the · separator) is the right fix, and it's nice that it also removes a fragile reliance on incidental whitespace rather than papering over it.
  • Tailwind v4 deferral is the right call given the Preflight/default changes (backdrop-blur, border color) — good instinct to keep this migration strictly additive/neutral and scope v4 as a separate visual-regression exercise.

Test coverage

  • No new unit tests were added, but none seem needed — this is a like-for-like infrastructure migration, and the existing 22 unit tests plus the route-parity/visual-regression verification described in the PR body is the right kind of coverage for this type of change (behavior-preserving upgrade, not new behavior).

Overall: well-researched, appropriately scoped, and the deferred items (Tailwind v4, unified() processor, dropping typescript@6) are correctly called out as follow-ups rather than silently punted. Nice work.

@plx

plx commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Addressed the worthwhile follow-ups in 6d53dd6:

  • Clarified that the second stripIndexId replacement is a defensive fallback for flat Markdown entries outside the documented folder convention.
  • Changed @typescript/native-preview from a caret range to an exact dated snapshot and updated the maintenance note so the manifest matches the intended pinning policy.
  • Kept the optional body fallback as-is: readingTime explicitly clamps empty input to 1 min read, with unit coverage, so it cannot degrade to 0 min read. The full build also rendered all 24 routes successfully.

Validation: npm run test:ci passes (lint, TypeScript 7 checks, 22 unit tests, spell/prose checks, Astro check/build, 24 generated pages, link validation, and RSS validation).

@github-actions

Copy link
Copy Markdown
Contributor

PR Validation ✅

All checks passed! Ready for review.

Checks Performed:

✓ Linting
✓ Unit tests
✓ Spell check (source)
✓ Prose lint (Vale)
✓ Vale fixture tests
✓ Type checking & Build
✓ Spell check (HTML)
✓ Internal link validation
✓ RSS feed validation
✓ Artifact upload

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review: Astro 7 / TypeScript 7 migration

Solid, well-documented migration — the PR description's verification section (route parity, visual regression, astro check clean) gives a lot of confidence for a two-major-version jump. A few notes:

Code quality

  • src/content.config.ts: the stripIndexId helper and withCardTitle generic are clean, well-commented replacements for the legacy type: "content" schemas. Good call preserving URLs with a custom generateId rather than accepting a breaking change.
  • postcss.config.mjs's comment says the generated CSS is "byte-for-byte identical" to the old @astrojs/tailwind output, but the PR description itself notes minifier-level differences (property reordering, flex:1 1 0%flex:1, rule-grouping). Worth softening the comment (e.g. "functionally identical") so it doesn't mislead someone diffing the CSS output later.
  • .replace(/\/index\.mdx?$/, "").replace(/\.mdx?$/, "") in stripIndexId is a little terse — a one-line comment already explains the folder case, but it might be worth a tiny inline example (my-post/index.md -> my-post) right at the fallback branch too, since that's the less-obvious path.

Potential bugs / risk areas

  • readingTime(post.body ?? "") / readingTime(project.body ?? "") — good defensive fallback now that body is optional under the Content Layer API, but it's silently falling back to a 0-length reading time rather than asserting body is present. If body is ever legitimately missing for a published post, this would silently show "0 min read" instead of surfacing a build-time error. Probably fine given glob() always populates body for markdown loaders, just flagging as a spot where a real bug upstream could go unnoticed.
  • Briefs collection relies on the default generateId behavior to match the legacy per-segment slugifier ("Keep the default ID generator..." comment) — this is exactly the kind of assumption that's easy to silently break on a future Astro bump. Given the manual verification already done for this PR, might be worth a cheap regression test (see below) so a future upgrade doesn't need another full manual audit.

Test coverage

  • No unit test was added for stripIndexId itself, even though it's pure, easily testable logic with a couple of non-obvious edge cases (folder index.md vs. flat <slug>.md fallback). A few vitest cases here would be cheap insurance against a future regression, especially since the correctness of every blog/project URL depends on it.
  • The route-parity and visual-regression checks described in the PR body are great, but they're manual/one-time verification, not part of the repeatable test suite. Consider whether the link/route parity check could be captured as a lightweight assertion (e.g. expected route count/shape) in CI rather than living only in the PR description.

Dependencies / maintenance

  • Pinning @typescript/native-preview to an exact dated dev snapshot (7.0.0-dev.20260707.2, no caret) is the right call for a preview package, and it's already called out as a follow-up to refresh/remove once TS7 stabilizes — good forward planning.
  • Removing accessible-astro-components and the @astrojs/check/typescript override cleans up now-dead config; confirmed no remaining references to the removed package.

Security / performance

Nothing concerning — this is a toolchain/version bump with no new runtime surface area, and the described verification (identical routes, pixel-parity screenshots) is a good substitute for a runtime security review here.

Overall: thorough migration with unusually good self-verification. The two items I'd actually want addressed before merge are the misleading "byte-for-byte identical" comment in postcss.config.mjs, and a couple of unit tests for stripIndexId given how much URL-stability weight it carries.

@plx

plx commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Addressed the two requested follow-ups from the latest automated review in b9ab8d2:

  • Reworded the PostCSS comment to promise functionally identical styling while acknowledging minifier-level CSS differences.
  • Extracted stripIndexId into a testable helper and added six Vitest regression cases covering folder-based Markdown/MDX IDs, nested paths, flat-entry fallbacks, and non-Markdown input.

I did not hard-code a route count: the existing CI build and link validator exercise the current generated route set without making intentional content additions require test maintenance. The full npm run test:ci suite passes with 28 tests and all 24 generated pages validated.

@github-actions

Copy link
Copy Markdown
Contributor

PR Validation ✅

All checks passed! Ready for review.

Checks Performed:

✓ Linting
✓ Unit tests
✓ Spell check (source)
✓ Prose lint (Vale)
✓ Vale fixture tests
✓ Type checking & Build
✓ Spell check (HTML)
✓ Internal link validation
✓ RSS feed validation
✓ Artifact upload

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review

Solid, well-documented migration. The write-up is unusually thorough (route-parity + visual-regression verification, explicit rationale for each deferred item), and the code changes match what the description claims. A few notes:

Code quality / best practices

  • src/lib/contentId.ts (stripIndexId) is clean and the new regression test (contentId.test.ts) is a good addition, especially paired with the "Address Astro migration review feedback" / "Add content ID regression coverage" follow-up commits — nice that this went through a self-review pass before landing.
  • Good call keeping briefs on the default generateId rather than forcing stripIndexId onto it — the comment explaining why (flat <category>/<name>.md files don't need index-stripping) heads off confusion for the next reader.
  • The postcss.config.mjs and astro.config.mjs comments do a good job explaining why (Astro 7 incompatibility) rather than just what, which matches the CLAUDE.md guidance on comments.

Potential bugs / edge cases

  • stripIndexId's fallback branch (.replace(/\.mdx?$/, "")) only fully collapses to the bare slug for .../index.md; a stray non-index .md file dropped into a post/project folder (CLAUDE.md phrasing is "folders containing at least an index.md file", implying other files may co-exist) would mint a second collection entry at <slug>/<other-name> rather than being ignored or erroring. Not a regression (legacy collections likely had similar exposure) and doesn't affect any current content, but worth a mental note if a post folder ever gains a second markdown file.
  • readingTime(post.body ?? "") / readingTime(project.body ?? "") — worth double-checking these are purely type-level accommodations (Content Layer entries typing body as possibly undefined) and not masking a real case where body comes back empty at runtime; if it's the former (which the PR's route-parity verification suggests), this is fine as-is.

Performance

No concerns — the PostCSS-direct Tailwind pipeline is functionally the same pipeline @astrojs/tailwind ran internally, and the content-layer glob() loaders are Astro's supported replacement for the legacy API.

Security

Nothing notable. Dependency bumps are all to official @astrojs/*/upstream packages, no new attack surface (no new user-input handling, no new external calls).

Test coverage

Good targeted coverage for the riskiest part of this migration (ID generation, where a mistake would silently break URLs/SEO). One thing that's asserted in the PR description but not encoded as an automated check going forward: the 24-route parity / visual-regression comparison was presumably a one-time manual verification rather than a repeatable test. Not a blocker — just flagging that a future content-layer change won't get the same safety net unless a lighter-weight "route manifest" snapshot test is added later.

Maintenance note (already acknowledged in the PR)

Pinning @typescript/native-preview to an exact dated dev snapshot (7.0.0-dev.20260707.2, no ^) is inherently a manual-upkeep item — the PR and CLAUDE.md both call this out as a followup, so just reinforcing that it'll need periodic attention (and dev snapshots occasionally get pruned from the registry, which could break a future npm ci on a stale lockfile).

Overall: low-risk, well-verified upgrade. Nothing here blocks merging.

@plx
plx merged commit 3eaadd3 into main Jul 23, 2026
3 checks passed
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.

1 participant