From 2ab32a36c3a0e4dc9c6c051046ce32bf4d96b9cf Mon Sep 17 00:00:00 2001 From: Rootul Patel Date: Thu, 16 Jul 2026 00:16:42 -0700 Subject: [PATCH] fix: return a real 404 for nonexistent athlete pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A loading.tsx at app/src/app/athlete/[slug]/ created an implicit Suspense boundary, which made Next stream the shell with a 200 status before the page's notFound() ran. So a nonexistent athlete returned HTTP 200 with the not-found UI — a soft 404 that search engines can index as a real page. Remove the segment's loading.tsx so Next resolves the page (and notFound()) before flushing the status. Missing athletes now return a real 404; existing athletes still return 200. The edge shard fetch is fast, so the lost loading skeleton costs little. Add e2e/athlete-404.spec.ts as a regression guard (verified red with the loading.tsx present, green without). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DaVB6YHWz94LTY8AGVc88Q --- app/e2e/athlete-404.spec.ts | 22 ++++++++++++++++ app/src/app/athlete/[slug]/loading.tsx | 35 -------------------------- app/src/app/athlete/[slug]/page.tsx | 8 ++++++ 3 files changed, 30 insertions(+), 35 deletions(-) create mode 100644 app/e2e/athlete-404.spec.ts delete mode 100644 app/src/app/athlete/[slug]/loading.tsx diff --git a/app/e2e/athlete-404.spec.ts b/app/e2e/athlete-404.spec.ts new file mode 100644 index 0000000..e5428ef --- /dev/null +++ b/app/e2e/athlete-404.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; + +// Regression guard for the athlete-page soft-404. A loading.tsx at +// app/src/app/athlete/[slug]/ created an implicit Suspense boundary that made +// Next stream the shell with a 200 status before the page's notFound() ran, so +// a nonexistent athlete returned HTTP 200 with the not-found UI (a soft 404 — +// bad for SEO). Removing that loading.tsx lets Next resolve notFound() before +// flushing the status. These assertions fail (miss returns 200) if it comes back. + +test("nonexistent athlete returns a real 404 status", async ({ page }) => { + const res = await page.goto("/athlete/no-such-person--zz-x"); + expect(res?.status()).toBe(404); + await expect(page.getByText("Page not found")).toBeVisible(); +}); + +test("existing athlete returns 200 and renders the profile", async ({ page }) => { + // Rootul Patel is present in the committed athlete index (see + // athlete-search.spec.ts, which relies on the same athlete). + const res = await page.goto("/athlete/rootul-patel--us-m"); + expect(res?.status()).toBe(200); + await expect(page.getByText("Rootul Patel")).toBeVisible(); +}); diff --git a/app/src/app/athlete/[slug]/loading.tsx b/app/src/app/athlete/[slug]/loading.tsx deleted file mode 100644 index df8d537..0000000 --- a/app/src/app/athlete/[slug]/loading.tsx +++ /dev/null @@ -1,35 +0,0 @@ -export default function Loading() { - return ( -
- {/* Back link */} -
- - {/* Header */} -
-
-
-
- - {/* Race cards */} -
- {[1, 2, 3].map((i) => ( -
-
-
-
-
-
-
-
-
-
-
-
- ))} -
-
- ); -} diff --git a/app/src/app/athlete/[slug]/page.tsx b/app/src/app/athlete/[slug]/page.tsx index c29dbf2..9c8a7cc 100644 --- a/app/src/app/athlete/[slug]/page.tsx +++ b/app/src/app/athlete/[slug]/page.tsx @@ -13,6 +13,14 @@ import AthleteRaceList from "@/components/AthleteRaceList"; // Web APIs only, no fs/zlib (see @/lib/athlete-shards). export const runtime = "edge"; +// NOTE: deliberately no loading.tsx for this segment. A loading.tsx creates an +// implicit Suspense boundary that makes Next stream the shell with a 200 status +// before this page runs, so notFound() for a nonexistent athlete could only +// produce a soft 404 (HTTP 200 with the not-found UI) — bad for SEO. Without +// it, Next resolves the page (and notFound()) before flushing the status, so +// missing athletes return a real 404. See e2e/athlete-404.spec.ts. The shard +// fetch is fast on the edge, so the lost skeleton costs little. + interface PageProps { params: Promise<{ slug: string }>; }