fix(compare): clean 404 SSR by dropping loading.tsx + early hasSharedBenches gate - #621
Merged
Merged
Conversation
PR #613 tried to short-circuit invalid /compare/<a>-vs-<b> pairs via notFound() in generateMetadata, but Next 16 still streamed the loading.tsx skeleton with HTTP 200 and the metadata switched to noindex mid-stream. Crawlers indexed an empty 'Loading live measurements' shell with noindex across the whole ad-hoc surface. Two changes: 1. Add hasSharedBenches() to compare-compute (pure set arithmetic on already-loaded provider appearances) and call it in generateMetadata so a pair like alchemy-vs-helius (both providers exist, share zero benches) hits notFound() at the metadata phase. Without this the page body's shared.length === 0 check fired only after loading.tsx had streamed, so the response was 200 + skeleton + a noindex meta tag from not-found.tsx. 2. Drop loading.tsx. With it in place, Next.js streams the skeleton first and the eventual notFound() can never demote the status code to 404 (response headers are already sent). Without it, invalid pairs cleanly return HTTP 404 with the not-found.tsx body and curated pairs render in one pass after the data resolves. Cold ad-hoc pairs lose the skeleton but the KV pair cache makes subsequent hits warm, and curated pairs are prerendered anyway. Verified locally (pnpm build + pnpm start): /compare/alchemy-vs-quicknode -> 404 Page not found (quicknode missing) /compare/mobula-vs-coingecko -> 308 then 404 (coingecko missing) /compare/alchemy-vs-helius -> 404 Page not found (no shared bench) /compare/quicknode-vs-alchemy -> 308 to canonical /compare/binance-vs-bybit -> 200 real h1, 50 KB content /compare/ethereum-vs-solana -> 200 real h1, 63 KB content
4 tasks
Flotapponnier
added a commit
that referenced
this pull request
Jun 21, 2026
…#621) (#622) PR #613 left valid ad-hoc /compare/<a>-vs-<b> URLs serving HTTP 200 + a loading skeleton + noindex meta. With loading.tsx in place, Next 16 streams the skeleton with 200 before the page body's notFound() can demote the status. Plus generateMetadata only checked that both providers exist, missing the no-shared-bench case. This hotfix lands the same fix as PR #621 (dev) but inline on main's older file structure (no extracted compare-compute helper module yet). hasSharedBenches() is a pure set arithmetic helper on appearances, no Prom fan out. loading.tsx removal lets notFound() ship a real 404. Verified locally: /compare/alchemy-vs-quicknode -> 404 (quicknode missing) /compare/mobula-vs-coingecko -> 308 then 404 (coingecko missing) /compare/alchemy-vs-helius -> 404 (no shared bench) /compare/quicknode-vs-alchemy -> 308 to canonical /compare/binance-vs-bybit -> 200 real h1 (valid ad-hoc) /compare/ethereum-vs-solana -> 200 real h1 (curated)
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.
Root cause
PR #613 tried to short-circuit invalid /compare/-vs- pairs via notFound() in generateMetadata. The metadata switched to noindex but Next 16 still streamed the loading.tsx skeleton with HTTP 200, so crawlers indexed an empty 'Loading live measurements' shell with a noindex meta tag across the entire ad-hoc surface.
Two problems compounded:
The page body's shared.length === 0 check (e.g. alchemy-vs-helius: both exist but share zero benches) fired AFTER loading.tsx had streamed. generateMetadata only checked provider existence, not bench overlap.
With loading.tsx in place, ANY notFound() resolves to 200 + skeleton body + late noindex switch, because the response headers are already sent before the Suspense child resolves.
Curl evidence on prod (origin/main f6e306b):
Fix
Add hasSharedBenches() to compare-compute (pure set arithmetic on already-loaded provider appearances, no Prom fan out). Call it in generateMetadata so pairs with both providers but no overlap notFound() at the metadata phase.
Drop loading.tsx. With it gone, notFound() from either generateMetadata or the page body cleanly demotes the response to HTTP 404 with not-found.tsx body. Curated pairs prerender at build time so they're unaffected. Cold ad-hoc pairs lose the skeleton but the KV pair cache (compare-cache.ts) makes subsequent hits warm, and middleware 308s reverse-pair URLs to canonical, so most requests hit the prerendered or cached entry.
Verification (local pnpm build + pnpm start)
pnpm typecheck, pnpm lint and pnpm test all clean.
Test plan