|
| 1 | +# pages-router-complex |
| 2 | + |
| 3 | +A deliberately convoluted Pages Router app. It exists as a compatibility |
| 4 | +target for vinext: the patterns here are the kind that only surface in big, |
| 5 | +old, enterprise pages-router codebases. It is set up for Cloudflare the way |
| 6 | +`vinext init` scaffolds it (vinext + cloudflare Vite plugins, KV data cache, |
| 7 | +Workers CDN cache, Images optimizer, `wrangler.jsonc` with the |
| 8 | +`vinext/server/fetch-handler` worker entry). |
| 9 | + |
| 10 | +**It is expected to run correctly under real Next.js (`pnpm dev:next`).** |
| 11 | +Running it under vinext (`pnpm dev`) is the goal, not (yet) the guarantee — |
| 12 | +the e2e suite in `tests/e2e/pages-router-complex/` documents the target |
| 13 | +behaviour and may fail under vinext today. |
| 14 | + |
| 15 | +## The patterns being exercised |
| 16 | + |
| 17 | +- **`pageExtensions: ["page.tsx", "page.ts"]`** — every routable file uses the |
| 18 | + suffix, non-page helpers live alongside pages (`pages/shell-initial-props.ts`), |
| 19 | + and `middleware.page.ts` / `instrumentation.page.ts` carry the suffix too. |
| 20 | +- **App-shell `getInitialProps`** (disables automatic static optimisation) that |
| 21 | + fetches masthead/baseboard chrome through an in-memory TTL memoiser, skips |
| 22 | + the fetch entirely for embedded-shell (cookie-flagged) requests, bypasses the |
| 23 | + memo in draft mode, reads env-based settings, and emits a beacon when it |
| 24 | + re-runs client-side on navigations. |
| 25 | +- **Extra named exports from special files**: `_app` re-exports a conditional |
| 26 | + top-level-await `outboundStub` promise; `middleware.page.ts` exports an |
| 27 | + unrelated async loader alongside `middleware` + `config`. |
| 28 | +- **Class-based `_document`** with its own `getInitialProps` (palette derived |
| 29 | + from `ctx.req.url`, `<html lang>` from the zone), `beforeInteractive` |
| 30 | + scripts (external + `dangerouslySetInnerHTML` bootstrap), raw inline |
| 31 | + `<script>` tags, and data attributes on `<body>`. |
| 32 | +- **Middleware pipeline** (regex `matcher` form): CDN-prefix rewrite |
| 33 | + (`/atlas/cdn/_next/*` → `/_next/*`), a 403 for `/_next/image`, a JSON |
| 34 | + `hardNavTo` response for raw `/_next/data` requests |
| 35 | + (`skipMiddlewareUrlNormalize`), editor-draft redirect on a request header, |
| 36 | + draft-cookie scrubbing for API routes via `NextResponse.next({ request })`, |
| 37 | + then a single-segment zone rewrite/redirect with an `x-zone` response |
| 38 | + header. |
| 39 | +- **A `[zone]` route dimension** hidden from public URLs for the home zone, |
| 40 | + feeding a real **i18next/react-i18next** runtime (one instance per tree, |
| 41 | + synchronous init for SSR, per-zone bundles with fallback) and a zone-aware |
| 42 | + `next/link` wrapper for all chrome links. |
| 43 | +- **Catch-all + siblings**: `gallery/[...facets]` with static siblings that |
| 44 | + must win precedence (`gallery/curated/first`, `gallery/directory/a-z`), |
| 45 | + facet-dedupe redirects, character-scrub redirects, permanent deep-trail |
| 46 | + collapses, cacheable 404s, and per-wall surrogate TTL overrides. |
| 47 | +- **A dynamic/static/dynamic route sandwich** (`[collection]/item/[assetId]`) |
| 48 | + whose page-data function branches into three templates off what the |
| 49 | + catalogue record says the asset is (withdrawn records and malformed ids |
| 50 | + 404 first). |
| 51 | +- **`getServerSideProps` wrapped in a metering HOF** on every page, custom CDN |
| 52 | + cache headers (`Surrogate-Control`/`Surrogate-Key`), a conditional CSP |
| 53 | + response-header side effect, and one page with **no data-fetching function |
| 54 | + at all** (`detail-tools/client-flags`). |
| 55 | +- **Server-snapshot data layer**: gSSP runs ops through a server handle that |
| 56 | + records a snapshot, the snapshot rides page props, and the browser handle |
| 57 | + is seeded with it so hydration reads from memory (`useGraphOp`). |
| 58 | +- **Client-side routing machinery**: shallow `router.push` with the internal |
| 59 | + dynamic-route pattern as `pathname` and the public URL as `as`; |
| 60 | + `router.events` driving a transition overlay with a failsafe timeout; a |
| 61 | + router-agnostic reset hook on `next/compat/router` + `usePathname`. |
| 62 | +- **App Router hooks in Pages Router pages**: `useSearchParams` seeding |
| 63 | + initial state and `usePathname`, combined with raw |
| 64 | + `window.history.replaceState` query updates that bypass the router. |
| 65 | +- **`next/image` with a custom loader** in `fill` mode + css-module class on |
| 66 | + the fault screens — the framework optimizer endpoint is never used, which |
| 67 | + is what makes the middleware's `/_next/image` 403 safe. |
| 68 | +- **API routes**: draft-mode gateway with landing-path resolution, a |
| 69 | + bearer-gated memo purge endpoint, a cookie-reflecting upstream relay proxy |
| 70 | + in promise-chain style, a legacy path rewritten in `afterFiles`, and a |
| 71 | + 204-with-cache-headers type-ahead shim. |
| 72 | +- **`next.config.js` (CJS)** exporting the **function form** — an async |
| 73 | + function of `(phase, context)` built by a decorator composer — with a |
| 74 | + throwing `generateBuildId`, prod-only `assetPrefix`, env-conditional |
| 75 | + `fallback` rewrites, and a webpack hook (workspace alias + test-module |
| 76 | + replacement via `NormalModuleReplacementPlugin`). |
| 77 | +- **`instrumentation.page.ts`** gated on `NEXT_RUNTIME === "nodejs"`, with its |
| 78 | + effect observable through `/api/status`. |
| 79 | + |
| 80 | +## Running |
| 81 | + |
| 82 | +```bash |
| 83 | +pnpm dev # vinext dev server (needs RELEASE_TAG for now, see below) |
| 84 | +pnpm dev:next # real Next.js dev server (ground truth; --webpack, see below) |
| 85 | +pnpm build # vinext build (RELEASE_TAG is required and set inline) |
| 86 | +pnpm build:next # next build |
| 87 | + |
| 88 | +# The behaviour suite (server starts under vinext automatically): |
| 89 | +PLAYWRIGHT_PROJECT=pages-router-complex pnpm run test:e2e |
| 90 | +``` |
| 91 | + |
| 92 | +## Known findings |
| 93 | + |
| 94 | +- **Next.js 16.2.7 + Turbopack** fails to compile: the global-CSS-in-_app |
| 95 | + validation false-positives when `pageExtensions` renames `_app` to |
| 96 | + `_app.page.tsx`. The `dev:next`/`build:next` scripts pass `--webpack`. |
| 97 | +- **TypeScript 7 (native preview)**: Next's own tsconfig-paths support |
| 98 | + degrades under it, so the `@atlas/*` alias is wired into both bundlers |
| 99 | + explicitly (webpack hook + Vite `resolve.alias`); tsconfig `paths` (without |
| 100 | + the removed `baseUrl`) stays authoritative for the type checker. |
| 101 | +- **vinext dev (Cloudflare plugin): 59/73 specs pass; the 14 known gaps are |
| 102 | + marked `test.fixme` so the passing surface runs in CI.** Known gaps: |
| 103 | + `generateBuildId` is invoked at dev startup (Next.js only calls it at build |
| 104 | + time — the e2e server exports `RELEASE_TAG` to compensate), shallow routing |
| 105 | + + `router.events` (including a hydration knock-on that breaks page |
| 106 | + interactivity), the `next/image` custom-loader/`fill` path, raw |
| 107 | + `/_next/data` interception and the `/_next/image` 403 middleware branches, |
| 108 | + the `history.replaceState`-beside-the-router hybrid, the gallery scrub |
| 109 | + redirect, cacheable-404 surrogate headers, the `afterFiles` type-ahead |
| 110 | + rewrite, and the purge endpoint pair (possibly env vars not reaching the |
| 111 | + workerd runtime). |
| 112 | +- The pinned workerd binary caps `compatibility_date` at 2026-04-08, so |
| 113 | + `wrangler.jsonc` pins 2026-04-01 rather than the scaffold's "today". |
| 114 | + |
| 115 | +## Layout |
| 116 | + |
| 117 | +- `lib/` — the app's internal platform libraries, consumed through tsconfig |
| 118 | + path aliases (`@atlas/*`) the way a monorepo app consumes shared packages: |
| 119 | + zones (zone routing + i18next runtime), edge-policy (CDN headers), memo |
| 120 | + (TTL result cache), beacon (metrics/RUM), graph-handle (data layer), chrome |
| 121 | + (masthead/baseboard), wiring + blocks (the provider pyramid and frame), |
| 122 | + trials, draft, and so on. |
| 123 | +- `helpers/`, `surfaces/` — app-level page-data helpers and page templates. |
| 124 | +- `pages/` — the route tree described above. |
0 commit comments