Migrate Pages Router to App Router - #267
Draft
cooperability wants to merge 2 commits into
Draft
Conversation
…TODOs Routes move from `src/pages/**` to `src/app/**`. Every page is a server component; interactivity is pushed into named `'use client'` leaves rather than marking whole routes client-side. Behaviour intended to change: - `/api/hello` is deleted (was an unused scaffold). A replacement would now be an `app/api/*/route.ts` Route Handler. - The homepage gains a Mandelbrot Explorer CTA beside the Prompt Composer one. - Canonical host standardises on `https://www.cooperability.com`, matching `next-sitemap.config.js`. The old homepage canonical pointed at the apex. - `yarn access` scores will move: collapsing two nested `next-themes` providers into one made `NEXT_PUBLIC_AXE_FORCE_THEME` effective for the first time. Behaviour deliberately preserved, and verified against a production build: per-route `og:title`, favicon and `apple-mobile-web-app-capable` on all seven routes; unknown resource slugs 404 rather than 500; `/` stays in the sitemap (12 URLs); the homepage quote still varies per request. `next build` is pinned to `--webpack`: Next 16 defaults to Turbopack, which cannot resolve `next/package.json` under Yarn PnP. This unblocks the build without pre-empting the pnpm migration. Also folded in, since all of it was making the working tree dirty on every build or cross-OS install: - Untrack `tsconfig.tsbuildinfo`, `accessibility-reports/` and `public/sw.js` (the latter two were already gitignored yet tracked), and ignore platform-native `.yarn/cache` archives. A Windows `yarn install` had staged win32 `sharp`/`swc` binaries over the linux ones Vercel builds against. - Track `next-env.d.ts` deliberately instead of ignoring-yet-tracking it; `yarn typecheck` needs its CSS-module and image declarations. - Documentation WIP for `docs/Tooling.md` and the Prompt Composer README. Migration nuances that cost real debugging time are written up in README.md under "App Router migration notes" rather than left in this message. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
cooperability
added a commit
that referenced
this pull request
Aug 26, 2026
Fourth single-variable attempt at the failing preview deploy, and the first one aimed at the builder rather than the package manager. The App Router branch (#267) explicitly pins `next build --webpack`, which is a deliberate opt-out of the Next 16 default. The most likely reason to add that flag is that Turbopack does not build this project on Vercel -- which would mean the failing deploys here have nothing to do with pnpm at all, and everything to do with this branch restoring the default builder while removing the `ls -la .yarn` debug probes from the same script. If this deploy goes green, the builder was the cause and the pnpm migration was never implicated. That also revises the recommendation in docs/PNPM-MIGRATION.md section 8: Turbopack's 2.4x faster build is not available here until whatever breaks it on Vercel is understood. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013RtsQWaEaAe4iVnSoUCDfY
cooperability
added a commit
that referenced
this pull request
Aug 26, 2026
Pinning --webpack did not fix the preview deploy, so the builder is not implicated and there is no reason to give up Turbopack's 2.4x faster build (measured, both sides, in docs/PNPM-MIGRATION.md section 8). Parking the Vercel failure here rather than continuing to guess. Four hypotheses were each tested by pushing them alone, and each was wrong: 1. corepack not enabled -> restored ENABLE_EXPERIMENTAL_COREPACK=1 2. engines.node semver range -> back to the "22.x" major selector 3. pnpm-workspace packages: [.] -> removed the monorepo signal 4. Turbopack failing on Vercel -> pinned --webpack Changes 1-3 are correct regardless and are kept; 4 is reverted here. What is known: main and PR #267 both deploy successfully, and every commit on this branch fails, so the cause is on this branch. GitHub Actions runs the same install and the same production build on ubuntu and passes in ~60s, so it is specific to the Vercel environment rather than to pnpm or the build. What is needed: the build log, which cannot be read from here -- there is no Vercel CLI and no token in this environment. One command answers it: npx vercel login && npx vercel inspect <deployment-url> --logs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013RtsQWaEaAe4iVnSoUCDfY
The App Router migration landed without a working lint gate. Restoring it turned out to also mean restoring the dev server, and both then surfaced real defects — so this is one causal chain rather than three unrelated changes. `yarn lint` ran zero rules; it crashed. ESLint 10 is ahead of this stack: eslint-plugin-react, eslint-plugin-jsx-a11y and eslint-plugin-import have no ESLint 10 release at all, so there was nothing to upgrade to. Pinned to 9.39.5, which every plugin supports — not a weakening, since the alternative was no gate. `eslint-config-next` 16 ships a native flat config, so the FlatCompat shim is gone along with `@eslint/eslintrc` and the never-imported `@eslint/compat`, and React version detection is pinned rather than autodetected (detection calls an API v10 removed). `yarn dev` was broken outright, for the reason `next build` already had a workaround for: Turbopack cannot resolve `next/package.json` under Yarn PnP, and only `build` had been pinned to `--webpack`. `dev` and `analyze` now pin it too. `analyze` needed it regardless — `@next/bundle-analyzer` is a webpack plugin that Turbopack ignores. Fixing `dev` generated `.next/dev/types` for the first time, which immediately caught `layout.tsx` exporting `siteTitle`. Next type-checks route files against a fixed set of allowed exports, so that had been a latent build error nothing could see. With lint running, five `react-hooks/set-state-in-effect` errors surfaced. All are genuine extra render passes, and all are fixed at the pattern level rather than suppressed: - `useHydrated` (new, built on `useSyncExternalStore`) replaces the `useState(false)` + `useEffect(() => setMounted(true))` idiom in ThemeSwitch and ActiveIcon, which cost a second render on every mount. - `useResponsive` reads the viewport through `useSyncExternalStore` rather than seeding it from an effect, keeping the debounced resize subscription. - OpioidConverter derives both equivalences with `useMemo`. They had been mirrored into state, so every dose change painted the previous total first. - PromptComposer adjusts its edit buffer during render instead of re-syncing in an effect, which also removes a flash of the empty-state placeholder. Covered by 18 new tests over the dosage arithmetic and the viewport hook, each proven able to fail by mutating the code under test and confirming red. Also in this pass: - Security headers on every route: `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options`, `Permissions-Policy` and HSTS. No `preload` on HSTS, which is impractical to reverse. The CSP ships as `Content-Security-Policy-Report- Only`: next-themes and Next's own bootstrap both inject inline script, so enforcing it needs a per-request nonce, which would force dynamic rendering on every route. - The service worker precached nothing usable. It globbed `.next` filesystem paths, so entries like `static/chunks/x.js` resolved to `/static/chunks/x.js` and 404'd, and it also swept in `.next/server` and `.next/cache`, neither of which is reachable over HTTP. Now 42 entries under `/_next/static/`, each verified to return 200. - Remove `prop-types`; it was never imported. The blocker recorded against it did not exist: `YARN_CACHE_FOLDER` is set in `vercel.json` and locally, so Yarn writes outside the tracked `.yarn/cache` and dependency changes produce no cache churn. `yarn access` is unblocked but not re-measured — it now gets past lint and fails only for want of a Chrome binary in the sandbox this was fixed in. Co-authored-by: Cursor <cursoragent@cursor.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 free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the App Router item in the README modernization TODO, plus the adjacent ones it turned out to depend on.
What changed
Routes move from
src/pages/**tosrc/app/**.src/pagesis gone entirely. Every page is a server component; interactivity is pushed down into named'use client'leaves (quote-box.tsx,resources-list.tsx, one*-client.tsxper applet) rather than marking whole routes client-side.Net -1633 lines across 59 files, mostly because
getStaticProps/getServerSidePropsplumbing and thegetLayoutpattern collapse into the file conventions.The nuances, which is the actually interesting part
These are written up in full in
README.mdunder App Router migration notes. The ones that cost real debugging time:Metadata inheritance replaces, it does not deep-merge. Setting
icons: { apple: … }on a page silently drops the layout'sicon. That is how the favicon vanished from the three applet routes mid-migration — and the check that "found" it again was reading a stale dev server on a port I thought I'd killed. Every metadata claim in this PR was re-verified against a fresh production build on an unused port.openGraph.titlehas its own resolution chain and never derives fromtitle. Atitle.templateon the root layout looks like the fix but only applies to children that set their ownopenGraph.title; children setting justtitleinherit the parent's resolveddefaultverbatim. Five routes were serving the bare site title as their share card before I set each one explicitly. Worth notingmainhad the same bug hidden — it emitted<meta name="og:title">, and OG scrapers only readproperty, so those tags were being ignored outright.generateStaticParamsis notgetStaticPaths: { fallback: false }. It defaults todynamicParams: true, so an unknown slug renders on demand and 500s where it used to 404. Needs an explicitexport const dynamicParams = false.next/dynamicwithssr: falseis illegal in a server component. Hence the serverpage.tsx(which ownsmetadata) plus thin client wrapper per applet, rather than one merged client route.force-dynamicis load-bearing, and its absence is silent. Without it the homepage prerenders and the per-request quote freezes at build time — no warning, just a page that stops changing.next-sitemapnever readsapp-build-manifest. Aforce-dynamicroute appears in neither manifest it does read, so/dropped out of the sitemap. Restored viaadditionalPaths.public/sitemap*.xmlis gitignored, so this class of regression is structurally invisible to code review — the reason it's a README note and not just a fix.next-themesshort-circuits a nested provider to a Fragment, so the inner one's props had been dead code. Collapsing to one provider madeNEXT_PUBLIC_AXE_FORCE_THEMEtake effect for the first time — expectyarn accessnumbers to move, and treat the new figures as the baseline.Verified against a production build
yarn typecheckandyarn jest --ciclean. Per-route checks againstnext start:og:titledistinct per routeapple-mobile-web-app-capable/Behaviour that intentionally changed
/api/hellois deleted — unused scaffold; a replacement would now beapp/api/*/route.ts.www, matchingnext-sitemap.config.js. The old homepage canonical pointed at the apex, and the JSON-LD disagreed with both. If the apex is what you actually want, that's one line inmetadataBaseplus the sitemap config — flagged as a TODO.Build system
next buildis pinned to--webpack. Next 16 defaults to Turbopack, which cannot resolvenext/package.jsonunder Yarn PnP — plainnext buildfails outright. This unblocks the build without pre-empting the pnpm migration the README already argues for.yarn lintis broken onmainand stays broken here: an ESLint/typescript-eslintmajor mismatch throwsClass extends value undefined. Out of scope, left as a TODO, but it means jsx-a11y did not run on this diff — worth knowing when reviewing the client boundaries.Repo hygiene folded in
All of this was dirtying the working tree on every build or cross-OS install, which is what made the migration diff hard to read in the first place:
tsconfig.tsbuildinfo,accessibility-reports/andpublic/sw.js— the latter two were already gitignored yet tracked, so the ignore rule was doing nothing..yarn/cachearchives. A Windowsyarn installhad staged win32sharp/swc/unrs-resolverbinaries over the linux ones Vercel builds against (~49 MB each way). Committing that set would likely have broken the next deploy's image optimization and compilation, so it is deliberately not in this PR.next-env.d.tswas the mirror-image problem — ignored yet tracked, and rewritten by every build. Now tracked deliberately, becauseyarn typecheckneeds its CSS-module and image declarations.Known gaps, filed not fixed
prop-typesremoval deferred — dropping a dep regenerates.pnp.cjs/yarn.lockand writes new tracked.yarn/cachezips, which belongs in its own PR.@vercel/og) yet;og:titleis correct but there's no card art.Draft rather than ready-for-review: the
yarn accessbaseline needs re-measuring now that theming actually works, and I'd rather you sanity-check thewwwcanonical decision before this merges.Made with Cursor
Update: the toolchain repair (second commit)
The first commit noted that
yarn lintwas broken onmainand stayed broken, so jsx-a11y never ran on the migration diff. Fixing that turned into one causal chain worth reading in order, because each step uncovered the next.1. ESLint 10 is ahead of this stack
The crash (
TypeError: Class extends value undefined) looked like one stale package. It wasn't. Surveying every plugin's peer range:eslint-plugin-react^9.7eslint-plugin-jsx-a11y^9eslint-plugin-import^9eslint-plugin-react-hooks^10✅typescript-eslint^10✅Three of the plugins
eslint-config-nextpulls have no ESLint 10 release at all — includingjsx-a11y, the one this project most cares about. Soeslintmoves to 9.39.5. That is a downgrade, and it is not weakening the gate: the alternative was no gate at all.Two related things fell out:
FlatCompatis obsolete here.eslint-config-next16 ships native flat config ateslint-config-next/core-web-vitals; wrapping it inFlatCompatthrowsConverting circular structure to JSON. Dropping the shim removed@eslint/eslintrcand@eslint/compat(the latter was inpackage.jsonbut never imported).eslint-config-nextsetsreact: { version: 'detect' }, and detection calls an API v10 removed. Reading the installed version directly is immune to that and faster.eslint-config-prettieralso moved to the end of the array, where it can actually switch off the stylistic rules of the configs before it.2.
yarn devwas broken outrightRestoring lint exposed this:
yarn devfails completely under Yarn PnP, because Next 16 defaults to Turbopack and Turbopack can't resolvenext/package.json. This is the same root cause the first commit pinned--webpackfor — but onlybuildhad been fixed.devandanalyzenow pin it too.analyzeneeded it regardless, since@next/bundle-analyzeris a webpack plugin Turbopack ignores entirely.3. Which surfaced a latent build error
Getting
devrunning generated.next/dev/typesfor the first time, which immediately failed typecheck:layout.tsxexportedsiteTitle. Next type-checks route files against a fixed set of permitted exports. Nothing could see this before, because the types that enforce it were never generated.Worth knowing as a footgun:
yarn typecheckis stricter afteryarn devhas run, becausetsconfig.jsonincludes.next/dev/types/**.4. Five real render bugs, fixed at the pattern level
With lint running,
react-hooks/set-state-in-effectflagged five genuine extra render passes. None are suppressed:useState(false)+useEffect(setMounted)useHydrated()onuseSyncExternalStore— no effect, no second passuseResponsiveuseSyncExternalStore, debounce preserveduseMemo; every dose change had painted the previous total first18 new tests, each proven able to fail: mutating
Math.pow(dose, 2)todose * 2and the resize debounce from 100ms to 900ms turned exactly the three intended assertions red, then restored green. The dosage arithmetic in particular is now locked down — it's the highest-consequence code in the repo and had no coverage.5. Other fixes in the same pass
Security headers on every route (closes a README TODO):
X-Content-Type-Options,Referrer-Policy,X-Frame-Options,Permissions-Policy, HSTS. Verified present on both root and nested routes.preload— that puts the apex on browser preload lists and is impractical to reverse. It should be a decision, not a side effect.Content-Security-Policy-Report-Only, and still needs promoting. It cannot be enforced as written:next-themesand Next's bootstrap both inject inline<script>, so a real policy needs a per-request nonce — which forces dynamic rendering on every route. Check a preview deploy's console, then decide if that trade is worth it.The service worker was precaching nothing.
build-sw.mjsglobbed the.nextfilesystem tree, so entries came out asstatic/chunks/x.jsand the browser requested/static/chunks/x.js. Confirmed 404 by direct request. It also swept in.next/serverand.next/cache, neither reachable over HTTP. Now 42 entries under/_next/static/, each verified 200.prop-typesremoved. The blocker recorded against it in the README didn't exist:YARN_CACHE_FOLDERis set invercel.jsonand in local shells, so Yarn writes outside the tracked.yarn/cacheand dependency changes produce no cache churn. (Corollary worth noting: zero-install is already not in effect for this repo.)Verification
lint·typecheck·jest(25 tests) ·build·yarn install --immutableall exit 0. Sitemap still 12 URLs. Working tree stays clean across a full build.Not verified, and I'd rather say so:
yarn accessnow gets past lint but can't complete here — the agent sandbox has no Chrome binary. Please run it locally. Treat the result as a new baseline rather than a regression, because collapsing the nestedThemeProviderin the first commit madeNEXT_PUBLIC_AXE_FORCE_THEMEeffective for the first time.Still open
public/assets; the manifest is scoped to.next/static, and there's still no offline fallback route.wwwvs apex canonical decision.loading.tsxstreaming boundaries yet — deliberately skipped, since all these routes are statically generated and the win is small.