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 }>; }