From 2437ffce42929bfe522299d41f4164c0cb2c7012 Mon Sep 17 00:00:00 2001 From: Tudor Date: Wed, 2 Sep 2026 16:11:48 +0100 Subject: [PATCH] refactor(app): move site routes into a (frontend) route group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Payload's admin panel ships its own root layout rendering html/body. Next allows multiple root layouts only when no app/layout.tsx exists, so the site's routes move into their own group. Route groups are invisible to routing: every public URL is unchanged, verified against the build's route table. The metadata file conventions deliberately stay at the app/ root. Moving them into the group renamed /icon.png to /icon-4usi79.png (likewise apple-icon and opengraph-image) and dropped /robots.txt altogether, which would have broken the /icon.png cache-control rule, the outputFileTracingIncludes entry for the share card, and robots.txt. darkThemeSafety reads app/globals.css off disk rather than importing it, so it needed its own path fix — a grep for import specifiers misses it, and it fails as an unrunnable suite rather than a failed assertion. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017YmbBhr8s7GusjDE12hrZM --- .../plans/2026-09-02-about-and-blog.md | 65 ++++++++++++++----- .../__tests__/api/proxyDenylist.test.ts | 2 +- nextjs-app/__tests__/app/metadata.test.ts | 8 +-- .../__tests__/app/placeMetadata.test.ts | 2 +- .../components/darkThemeSafety.test.ts | 4 +- .../__tests__/support/renderSchoolDetail.tsx | 2 +- .../app/{ => (frontend)}/admissions/page.tsx | 0 .../{ => (frontend)}/api/[...path]/route.ts | 0 .../app/{ => (frontend)}/compare/page.tsx | 0 nextjs-app/app/{ => (frontend)}/globals.css | 0 nextjs-app/app/{ => (frontend)}/layout.tsx | 0 nextjs-app/app/{ => (frontend)}/page.tsx | 0 .../app/{ => (frontend)}/rankings/page.tsx | 0 .../{ => (frontend)}/school/[slug]/page.tsx | 0 .../schools/[place]/[phase]/page.tsx | 0 .../{ => (frontend)}/schools/[place]/page.tsx | 0 .../schools/authority/[la]/[phase]/page.tsx | 0 .../schools/authority/[la]/page.tsx | 0 .../schools/near/[outcode]/page.tsx | 0 .../app/{ => (frontend)}/sitemap.xml/route.ts | 0 .../sitemaps/[...parts]/route.ts | 0 21 files changed, 57 insertions(+), 26 deletions(-) rename nextjs-app/app/{ => (frontend)}/admissions/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/api/[...path]/route.ts (100%) rename nextjs-app/app/{ => (frontend)}/compare/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/globals.css (100%) rename nextjs-app/app/{ => (frontend)}/layout.tsx (100%) rename nextjs-app/app/{ => (frontend)}/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/rankings/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/school/[slug]/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/schools/[place]/[phase]/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/schools/[place]/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/schools/authority/[la]/[phase]/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/schools/authority/[la]/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/schools/near/[outcode]/page.tsx (100%) rename nextjs-app/app/{ => (frontend)}/sitemap.xml/route.ts (100%) rename nextjs-app/app/{ => (frontend)}/sitemaps/[...parts]/route.ts (100%) diff --git a/docs/superpowers/plans/2026-09-02-about-and-blog.md b/docs/superpowers/plans/2026-09-02-about-and-blog.md index ec3249a..f1e8ff1 100644 --- a/docs/superpowers/plans/2026-09-02-about-and-blog.md +++ b/docs/superpowers/plans/2026-09-02-about-and-blog.md @@ -83,9 +83,21 @@ route to `/cms-api` is sufficient and complete. Do not add the exclusion. ## File Structure **Moved (Task 2)** — `app/*` → `app/(frontend)/*`, unchanged in content: -`layout.tsx`, `page.tsx`, `globals.css`, `robots.ts`, `opengraph-image.tsx`, -`icon.png`, `apple-icon.png`, `rankings/`, `admissions/`, `compare/`, -`schools/`, `school/`, `api/`, `sitemaps/`, `sitemap.xml/`. +`layout.tsx`, `page.tsx`, `globals.css`, `rankings/`, `admissions/`, +`compare/`, `schools/`, `school/`, `api/`, `sitemaps/`, `sitemap.xml/`. + +**Deliberately NOT moved — they stay at the `app/` root:** `robots.ts`, +`opengraph-image.tsx`, `icon.png`, `apple-icon.png`. + +Next.js metadata file conventions only produce stable root URLs at the `app/` +root. Inside a route group they are treated as segment-scoped: verified during +execution, moving them into `(frontend)` renamed `/icon.png` to +`/icon-4usi79.png`, `/apple-icon.png` to `/apple-icon-4usi79.png`, +`/opengraph-image` to `/opengraph-image-4usi79`, and dropped `/robots.txt` +entirely. That would have broken the `/icon.png` cache-control rule and the +`outputFileTracingIncludes['/opengraph-image']` entry in `next.config.mjs`, +and silently removed the site's robots.txt. Route handlers (`sitemap.xml/`, +`sitemaps/`, `api/`) are unaffected and move normally. **Created:** @@ -264,20 +276,25 @@ Write down the passing test count. It must be identical at Step 5. ```bash cd nextjs-app/app && mkdir -p "(frontend)" -git mv layout.tsx page.tsx globals.css robots.ts opengraph-image.tsx \ - icon.png apple-icon.png \ +git mv layout.tsx page.tsx globals.css \ rankings admissions compare schools school api sitemaps sitemap.xml \ "(frontend)/" ``` -Verify nothing is left behind — `app/` should now contain only `(frontend)`: +`robots.ts`, `opengraph-image.tsx`, `icon.png` and `apple-icon.png` stay at the +`app/` root — see "Deliberately NOT moved" in the File Structure section above. +Moving them silently changes their URLs and drops robots.txt. + +Verify the split — `app/` should now contain `(frontend)` plus exactly the four +metadata conventions: ```bash -cd /Users/tudor/projects/school_compare/nextjs-app && ls app +cd /Users/tudor/projects/school_compare/nextjs-app && ls -A app ``` -If anything else appears, move it too. `git status --short` is the -authoritative check: untracked files do not show in `git diff --stat`. +Expected: `(frontend)`, `apple-icon.png`, `icon.png`, `opengraph-image.tsx`, +`robots.ts`. If a route directory is still there, move it. `git status --short` +is the authoritative check: untracked files do not show in `git diff --stat`. - [ ] **Step 3: Update the four test imports** @@ -304,13 +321,27 @@ import { GET } from '@/app/(frontend)/api/[...path]/route'; - [ ] **Step 4: Find any other references to the old paths** +Two searches, because import specifiers are not the only way a file names a +path. Quote the `--include` globs or zsh expands them. + ```bash -cd /Users/tudor/projects/school_compare && grep -rn "app/layout\|app/page\|@/app/" \ - --include=*.ts --include=*.tsx --include=*.js --include=*.mjs \ +cd /Users/tudor/projects/school_compare +# 1. Import specifiers +grep -rn "app/layout\|app/page\|@/app/" \ + --include="*.ts" --include="*.tsx" --include="*.js" --include="*.mjs" \ nextjs-app --exclude-dir=node_modules --exclude-dir=.next | grep -v "app/(frontend)" + +# 2. Filesystem paths — readFileSync/path.join targets, which search 1 misses. +# __tests__/components/darkThemeSafety.test.ts reads app/globals.css this way +# and fails to *run* when the path is stale, so it shows as a failed suite +# rather than a failed assertion. +grep -rn "'app'" nextjs-app/__tests__ --include="*.ts" --include="*.tsx" \ + | grep -v "(frontend)" ``` -Expected: no output. Fix anything that appears. +Expected: no output from either. Fix anything that appears — note that +`renderSchoolDetail.tsx` names `app/layout.tsx` in a comment, which should be +updated for accuracy even though nothing breaks. - [ ] **Step 5: Verify tests, types and build** @@ -595,7 +626,7 @@ proxy's catch-all." indexed and must not be reachable through search results. **Files:** -- Modify: `nextjs-app/app/(frontend)/robots.ts` +- Modify: `nextjs-app/app/robots.ts` - Modify: `nextjs-app/next.config.mjs` - Modify: `nextjs-app/__tests__/app/nextConfig.test.ts` - Create: `nextjs-app/__tests__/app/robots.test.ts` @@ -609,7 +640,7 @@ indexed and must not be reachable through search results. Create `nextjs-app/__tests__/app/robots.test.ts`: ```ts -import robots from '@/app/(frontend)/robots'; +import robots from '@/app/robots'; describe('robots.txt', () => { it('disallows the admin panel and the CMS API', () => { @@ -651,7 +682,7 @@ rule exists. - [ ] **Step 3: Add the robots disallow entries** -In `nextjs-app/app/(frontend)/robots.ts`, change the `disallow` array to: +In `nextjs-app/app/robots.ts`, change the `disallow` array to: ```ts disallow: ['/api/', '/_next/', '/admin/', '/cms-api/'], @@ -1527,7 +1558,7 @@ without a webhook." - Create: `nextjs-app/components/blog/CalloutBlock.tsx` + `.module.css` - Create: `nextjs-app/app/(frontend)/blog/rss.xml/route.ts` - Create: `nextjs-app/app/(frontend)/content-sitemap.xml/route.ts` -- Modify: `nextjs-app/app/(frontend)/robots.ts` +- Modify: `nextjs-app/app/robots.ts` - Modify: `nextjs-app/lib/jsonld.ts` - Create: `nextjs-app/__tests__/app/blogMetadata.test.ts` - Modify: `e2e/tests/journeys.spec.ts` @@ -2123,7 +2154,7 @@ ${urls.map(({ loc, lastmod }) => - [ ] **Step 9: List both sitemaps in robots.txt** -In `nextjs-app/app/(frontend)/robots.ts`, change the `sitemap` key to: +In `nextjs-app/app/robots.ts`, change the `sitemap` key to: ```ts sitemap: [absoluteUrl('/sitemap.xml'), absoluteUrl('/content-sitemap.xml')], diff --git a/nextjs-app/__tests__/api/proxyDenylist.test.ts b/nextjs-app/__tests__/api/proxyDenylist.test.ts index 10731f2..083cd6b 100644 --- a/nextjs-app/__tests__/api/proxyDenylist.test.ts +++ b/nextjs-app/__tests__/api/proxyDenylist.test.ts @@ -8,7 +8,7 @@ // environment provides — under jsdom this suite fails on import, not on an // assertion. import { NextRequest } from 'next/server'; -import { GET } from '@/app/api/[...path]/route'; +import { GET } from '@/app/(frontend)/api/[...path]/route'; function request(path: string) { return new NextRequest(`http://localhost:3000/api/${path}`); diff --git a/nextjs-app/__tests__/app/metadata.test.ts b/nextjs-app/__tests__/app/metadata.test.ts index 44b0d57..800c19d 100644 --- a/nextjs-app/__tests__/app/metadata.test.ts +++ b/nextjs-app/__tests__/app/metadata.test.ts @@ -1,7 +1,7 @@ -import { metadata as homeMetadata } from '@/app/page'; -import { metadata as rankingsMetadata } from '@/app/rankings/page'; -import { metadata as admissionsMetadata } from '@/app/admissions/page'; -import { generateMetadata as compareMetadata } from '@/app/compare/page'; +import { metadata as homeMetadata } from '@/app/(frontend)/page'; +import { metadata as rankingsMetadata } from '@/app/(frontend)/rankings/page'; +import { metadata as admissionsMetadata } from '@/app/(frontend)/admissions/page'; +import { generateMetadata as compareMetadata } from '@/app/(frontend)/compare/page'; describe('canonical URLs', () => { it('the homepage canonicalises to the bare root', () => { diff --git a/nextjs-app/__tests__/app/placeMetadata.test.ts b/nextjs-app/__tests__/app/placeMetadata.test.ts index 7c9f27e..eb79b48 100644 --- a/nextjs-app/__tests__/app/placeMetadata.test.ts +++ b/nextjs-app/__tests__/app/placeMetadata.test.ts @@ -1,4 +1,4 @@ -import { generateMetadata as placeMeta } from '@/app/schools/[place]/page'; +import { generateMetadata as placeMeta } from '@/app/(frontend)/schools/[place]/page'; jest.mock('@/lib/places', () => ({ ...jest.requireActual('@/lib/places'), diff --git a/nextjs-app/__tests__/components/darkThemeSafety.test.ts b/nextjs-app/__tests__/components/darkThemeSafety.test.ts index b33ba70..39e2b93 100644 --- a/nextjs-app/__tests__/components/darkThemeSafety.test.ts +++ b/nextjs-app/__tests__/components/darkThemeSafety.test.ts @@ -109,7 +109,7 @@ describe('dark-theme safety', () => { * simply missed. */ describe('third-party surfaces under themed text', () => { - const GLOBALS = path.join(__dirname, '..', '..', 'app', 'globals.css'); + const GLOBALS = path.join(__dirname, '..', '..', 'app', '(frontend)', 'globals.css'); /** Leaflet surfaces our own code writes token-coloured text onto. */ const LEAFLET_POPUP_SURFACES = [ @@ -171,7 +171,7 @@ describe('third-party surfaces under themed text', () => { */ describe('destination tokens', () => { const css = fs.readFileSync( - path.join(__dirname, '..', '..', 'app', 'globals.css'), 'utf8'); + path.join(__dirname, '..', '..', 'app', '(frontend)', 'globals.css'), 'utf8'); const TOKENS = [ '--dest-sixthform', '--dest-sfcollege', '--dest-fecollege', diff --git a/nextjs-app/__tests__/support/renderSchoolDetail.tsx b/nextjs-app/__tests__/support/renderSchoolDetail.tsx index bc5b87b..acc657d 100644 --- a/nextjs-app/__tests__/support/renderSchoolDetail.tsx +++ b/nextjs-app/__tests__/support/renderSchoolDetail.tsx @@ -21,7 +21,7 @@ import { import { nationalAveragesFixture } from './schoolFixtures'; // The shell calls useComparison(), which throws outside the provider. In the -// app this wrapper comes from app/layout.tsx. +// app this wrapper comes from app/(frontend)/layout.tsx. function withProviders(ui: ReactNode) { return {ui}; } diff --git a/nextjs-app/app/admissions/page.tsx b/nextjs-app/app/(frontend)/admissions/page.tsx similarity index 100% rename from nextjs-app/app/admissions/page.tsx rename to nextjs-app/app/(frontend)/admissions/page.tsx diff --git a/nextjs-app/app/api/[...path]/route.ts b/nextjs-app/app/(frontend)/api/[...path]/route.ts similarity index 100% rename from nextjs-app/app/api/[...path]/route.ts rename to nextjs-app/app/(frontend)/api/[...path]/route.ts diff --git a/nextjs-app/app/compare/page.tsx b/nextjs-app/app/(frontend)/compare/page.tsx similarity index 100% rename from nextjs-app/app/compare/page.tsx rename to nextjs-app/app/(frontend)/compare/page.tsx diff --git a/nextjs-app/app/globals.css b/nextjs-app/app/(frontend)/globals.css similarity index 100% rename from nextjs-app/app/globals.css rename to nextjs-app/app/(frontend)/globals.css diff --git a/nextjs-app/app/layout.tsx b/nextjs-app/app/(frontend)/layout.tsx similarity index 100% rename from nextjs-app/app/layout.tsx rename to nextjs-app/app/(frontend)/layout.tsx diff --git a/nextjs-app/app/page.tsx b/nextjs-app/app/(frontend)/page.tsx similarity index 100% rename from nextjs-app/app/page.tsx rename to nextjs-app/app/(frontend)/page.tsx diff --git a/nextjs-app/app/rankings/page.tsx b/nextjs-app/app/(frontend)/rankings/page.tsx similarity index 100% rename from nextjs-app/app/rankings/page.tsx rename to nextjs-app/app/(frontend)/rankings/page.tsx diff --git a/nextjs-app/app/school/[slug]/page.tsx b/nextjs-app/app/(frontend)/school/[slug]/page.tsx similarity index 100% rename from nextjs-app/app/school/[slug]/page.tsx rename to nextjs-app/app/(frontend)/school/[slug]/page.tsx diff --git a/nextjs-app/app/schools/[place]/[phase]/page.tsx b/nextjs-app/app/(frontend)/schools/[place]/[phase]/page.tsx similarity index 100% rename from nextjs-app/app/schools/[place]/[phase]/page.tsx rename to nextjs-app/app/(frontend)/schools/[place]/[phase]/page.tsx diff --git a/nextjs-app/app/schools/[place]/page.tsx b/nextjs-app/app/(frontend)/schools/[place]/page.tsx similarity index 100% rename from nextjs-app/app/schools/[place]/page.tsx rename to nextjs-app/app/(frontend)/schools/[place]/page.tsx diff --git a/nextjs-app/app/schools/authority/[la]/[phase]/page.tsx b/nextjs-app/app/(frontend)/schools/authority/[la]/[phase]/page.tsx similarity index 100% rename from nextjs-app/app/schools/authority/[la]/[phase]/page.tsx rename to nextjs-app/app/(frontend)/schools/authority/[la]/[phase]/page.tsx diff --git a/nextjs-app/app/schools/authority/[la]/page.tsx b/nextjs-app/app/(frontend)/schools/authority/[la]/page.tsx similarity index 100% rename from nextjs-app/app/schools/authority/[la]/page.tsx rename to nextjs-app/app/(frontend)/schools/authority/[la]/page.tsx diff --git a/nextjs-app/app/schools/near/[outcode]/page.tsx b/nextjs-app/app/(frontend)/schools/near/[outcode]/page.tsx similarity index 100% rename from nextjs-app/app/schools/near/[outcode]/page.tsx rename to nextjs-app/app/(frontend)/schools/near/[outcode]/page.tsx diff --git a/nextjs-app/app/sitemap.xml/route.ts b/nextjs-app/app/(frontend)/sitemap.xml/route.ts similarity index 100% rename from nextjs-app/app/sitemap.xml/route.ts rename to nextjs-app/app/(frontend)/sitemap.xml/route.ts diff --git a/nextjs-app/app/sitemaps/[...parts]/route.ts b/nextjs-app/app/(frontend)/sitemaps/[...parts]/route.ts similarity index 100% rename from nextjs-app/app/sitemaps/[...parts]/route.ts rename to nextjs-app/app/(frontend)/sitemaps/[...parts]/route.ts