Skip to content

fix(compare): clean 404 SSR by dropping loading.tsx + early hasSharedBenches gate - #621

Merged
Flotapponnier merged 1 commit into
devfrom
fix/compare-ssr-investigate
Jun 21, 2026
Merged

fix(compare): clean 404 SSR by dropping loading.tsx + early hasSharedBenches gate#621
Flotapponnier merged 1 commit into
devfrom
fix/compare-ssr-investigate

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

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:

  1. 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.

  2. 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):

/compare/alchemy-vs-quicknode   200  <title>Page not found</title>  noindex  45977 bytes (loading shell)
/compare/mobula-vs-coingecko    308  -> coingecko-vs-mobula then 200 same shell
/compare/alchemy-vs-helius      200  <title>Alchemy vs Helius</title>  noindex  46516 bytes (loading shell)
/compare/quicknode-vs-alchemy   308  -> alchemy-vs-quicknode (works)
/compare/binance-vs-bybit       200  real h1, 66 KB content (works)
/compare/ethereum-vs-solana     200  real h1, 87 KB content (works)

Fix

  1. 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.

  2. 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)

/compare/alchemy-vs-quicknode   404 Page not found    (quicknode missing)
/compare/mobula-vs-coingecko    308 -> coingecko-vs-mobula then 404 (coingecko missing)
/compare/alchemy-vs-helius      404 Page not found    (no shared bench)
/compare/quicknode-vs-alchemy   308 to canonical      (middleware)
/compare/binance-vs-bybit       200 real h1, 50 KB    (valid ad-hoc)
/compare/ethereum-vs-solana     200 real h1, 63 KB    (curated)

pnpm typecheck, pnpm lint and pnpm test all clean.

Test plan

  • Merge to dev, hit a preview URL on a fresh ad-hoc bad pair and confirm 404 status code (not 200 + skeleton)
  • Confirm a curated pair (ethereum-vs-solana, codex-vs-mobula) still renders full SSR content with real h1 + 200 status
  • After cherry-pick to main and deploy, curl with cb=randomint to bypass cache and re-verify status codes

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
@Flotapponnier
Flotapponnier merged commit ef4ac46 into dev Jun 21, 2026
1 check passed
@Flotapponnier
Flotapponnier deleted the fix/compare-ssr-investigate branch June 21, 2026 14:09
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant