Migration to astro#265
Conversation
Astro 5 static scaffold: i18n routing (6 locales), react + sitemap integrations, redirect stubs, SVG icon sprite, base styles.
All routes from the Next app with exact URL parity: project pages (map + chart islands), data explorer (one island, SSR'd first cards), blogs via Content Collections, static pages, shared chrome. i18next core over the existing public/locales JSON.
Covers two Astro gaps: astro:assets aborts the whole build on one bad remote image (production data contains such images), and island hydration runtimes are force-inlined on every page (~57 MB across 14.6k pages).
blogs/images/ + relative refs let astro:assets optimize natively (incl. animated GIF -> animated webp), replacing the custom local-image pipeline.
18 unreferenced backup files; one had broken the i18next glob.
Trim the query to rendered fields (~20% smaller), paginate 100/page, refetch only projects whose modifiedAt/lastContributionDate changed (~50s -> ~6s warm). staticData.json doubles as the cache.
Kept as owned code (nothing else does render-skip). Activation now requires ASTRO_INCREMENTAL=1 from the orchestrator; the sitemap is regenerated over the merged tree. No-op 5s; one-project change 6s.
Hydration dedup: withastro/docs#2150, roadmap#36, astro#6247. Fail-soft images: no existing issue covers it.
Workspace package: generic recurrence-based discovery, one external file per distinct inline script, in-place replacement; configurable threshold/output/globs.
createRemoteImages(options): widths/quality/cache/concurrency and split onError/onTransientError policies. Failed images render a bundled error SVG and warn; cache layout unchanged (zero re-encode).
selectPaths page API + runIncrementalBuild(config). Hash globs, prune routes, sitemap locales, and build commands all move to config; the app orchestrator becomes a 34-line wrapper.
packages/README.md index; each package README links back to the decision records.
Setup, commands, cache/state table with cold costs, env knobs, structure, gotchas.
Strip change-log/session narration from comments; keep constraint rationale, contracts, and upstream references.
'activation' for the env-var mechanism, 'checklist' for the cutover go/no-go.
The branch now transforms the project in place instead of carrying a parallel astro/ subdir. Next app + its tooling deleted (rollback = main); locales/ moved out of publicDir; fetch-data runs under plain node (ts-node/codegen deps dropped); single root package.json with workspaces. Build verified end-to-end after the move.
32px PNG-in-ICO generated from logo-compact.svg; BaseLayout already linked /favicon.ico but the file never existed (404 under Next too).
node 22 + npm equivalent of the removed yarn/Next compose: mounts the repo, runs the dev server on 4321.
eslint 9 flat config (typescript-eslint, astro, react-hooks classic rules), stylelint-config-standard; both pass. Fixes surfaced by the first run: unused catch binding and dead assignment in fetchData, dead helper in DataExplorer, an irregular whitespace char. Pre-commit hooks call the npm scripts.
packages/ are plain directories imported by relative path — no linking, single node_modules. pnpm's strict resolution surfaced five phantom deps (the unified/remark markdown pipeline was only reachable through npm hoisting); now declared. sharp's build script approved via pnpm.onlyBuiltDependencies. Docker, pre-commit, README, and docs updated to pnpm.
Union narrowing on the mission cards, readonly params in chart.ts (the functions never mutate). pnpm settings moved to pnpm-workspace.yaml (settings-only, no packages key — not a workspace).
13 invariants from the migration's manual verification ladder — page and sitemap counts, data-JSON consistency, hydration file, blog-image and animated-webp guards, per-locale content, dist size early warning.
staticData.json is zod-validated at load (named-field errors instead of silently-empty pages on backend drift). i18n init warns once per build with empty-translation counts per locale.
Three tests against astro preview: map (client:only) mounts, history chart (client:visible) hydrates, data-explorer search filters the fetched dataset over SSR'd cards, blog images decode. Proves the islands hydrate from the deduped external runtime in a real browser — the check deferred since the hydration-dedup change.
There was a problem hiding this comment.
Why was this removed? We do need postcss
There was a problem hiding this comment.
Why remove "git config --global --add safe.directory /code"
|
|
||
| services: | ||
| next: | ||
| web: |
There was a problem hiding this comment.
"web" can be confusing as we use this to refer to web server in other projects
|
|
||
| checks: | ||
| build: . | ||
| command: sh -c 'pre-commit run -vvv --all-files' |
There was a problem hiding this comment.
Why remove precommit checks?
| "extends": "stylelint-config-standard", | ||
| "rules": { | ||
| "declaration-block-single-line-max-declarations": null, | ||
| "selector-class-pattern": null |
There was a problem hiding this comment.
Why remove the indent rule and concentric support
There was a problem hiding this comment.
Why remove unimported?
4-space indentation is the repo standard (2 for yaml).
PostCSS pipeline (flexbugs-fixes, nested, normalize, preset-env — same options; object-map form since Vite rejects Next's string-array format), main's design tokens (variables/globals), and all portable component + page modules co-located with their Astro counterparts. Islands share the page modules (hashed classes match across .astro and .tsx). de-card kept as a literal e2e hook. Not portable: JS-positioned Popup/DropdownMenu (CSS hover dropdown instead), next/font injection (font-family vars), some react-icons (text equivalents).
Class attributes were ~22% of every page with the default pattern.
GraphQL documents move to scripts/graphql/*.graphql (codegen's tag plucker cannot parse interpolated templates); fetchData reads the same files at runtime and uses the generated per-operation types. generated/ is no longer committed — 'pnpm generate:type' produces it from backend/schema.graphql, no running server needed.
Astro applies tsconfig paths natively; src imports use components/*, lib/*, layouts/*, i18n/*, styles/*, packages/* instead of relative chains. Boundary: files reachable from plain-node scripts keep relative imports (node does not read tsconfig). Codegen emits union types and operation types only (enums are not erasable; the full schema dump emitted duplicate enum names).
unimported cannot parse .astro entry points; knip understands them. Package public APIs are entry files (exports exempt). Removed two genuinely unused app exports it flagged.
Main's config (recommended + concentric + 4-space indentation) on stylelint 16: @Stylistic plugin supplies the removed indentation rule, stylelint-config-concentric-order the property order. 100+ order issues auto-fixed; one real specificity fix in the navbar dropdown.
eslint @stylistic/indent + stylelint @stylistic/indentation + editorconfig; 2,713 violations autofixed. Navbar dropdown state rule moved after its base rule (no-descending-specificity).
Minimal diff vs main: command/framework swaps, current env vars, updated translation + blog-image paths, and a short Build Internals appendix. Replaces the earlier full rewrite.
Astro's inlineStylesheets:'auto' duplicated ~10 KB of identical CSS into every page (~150 MB across 14.6k pages). External bundles are fetched once and cached; dist 643 -> 487 MB.
Content-hashed (stable across builds for the incremental merge), first char alphabetic, build fails on collision instead of silently merging styles. ~680 B/page.
| @@ -0,0 +1,29 @@ | |||
| // graphql-codegen wrapper: the typescript + typescript-operations plugin pair | |||
There was a problem hiding this comment.
There should be a configuration fix that solves this without a wrapper
There was a problem hiding this comment.
This should not have a diff
There was a problem hiding this comment.
Why do we need a eslint.config.mjs?
| // post-processes the final HTML after the other build:done hooks. | ||
| import hydrationDedup from './packages/astro-hydration-dedup/src/index.ts'; | ||
|
|
||
| const SITE = 'https://mapswipe.org'; |
There was a problem hiding this comment.
This cannot be hard-coded.
|
|
||
| const SITE = 'https://mapswipe.org'; | ||
|
|
||
| const CLASS_ALPHA = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; |
There was a problem hiding this comment.
Separate this into a separate plugin
Hero/Section/Card/CenteredCard/Heading/Tag/KeyFigure/ListItem/Button as .astro components (slots, zero JS); pages no longer hand-roll their markup. Output verified byte-equivalent across all 14,680 pages modulo CSS chunk order, attr order, and island prelude placement.
43 anchors and 21 wrapper divs move off raw CSS-module classes. Spread components drop the slot placement attr (Astro leaves it in props). Output byte-equivalent across all 14,680 pages.
Section/Card/Tag/Button/Link/Heading/ImageWrapper as .tsx twins of the .astro components (islands can't render .astro); DataExplorer and ProjectsMapIsland drop their hand-rolled markup. Shared cs() joiner in lib/cs. SSR output identical except class order inside one attribute.
Style-bearing components move to Name/{index.astro,index.tsx,
styles.module.css}; no-CSS components stay flat. CLASS_MAP_OUT debug
dump added (class hashes shift when a module moves — the map lets two
builds be compared structurally; verified identical on all pages).
House rule (matches main's JSX style): 2+ attributes each get their own line, closing bracket tag-aligned; single attribute stays inline. Enforced for tsx via @Stylistic jsx rules. Also drops the 'port of the Next component' comment phrasing (why/how comments only) and dedups the map placeholder in DataExplorer. Whitespace-only in the built HTML.
No description provided.