fix(compare): restore SSR content and canonical, 301 reverse-pair to alphabetical - #613
Merged
Merged
Conversation
This was referenced Jun 21, 2026
Flotapponnier
added a commit
that referenced
this pull request
Jun 21, 2026
) 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
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.
Cherry-pick of PR #612 (against dev) to main as a production hotfix.
Root cause
Live SSR audit on /compare/-vs- showed every URL returning HTTP 200 with the homepage title, no canonical, and the loading.tsx skeleton as body. Two problems compounded:
generateMetadatareturned an empty{}for any pair where the slug was non-canonical or where either provider didn't resolve. With no per-page metadata, Next.js used the root layouttitle.default, so crawlers saw the homepage shell on every broken /compare URL.The page calls
redirect()(non-canonical slugs) andnotFound()(invalid pairs) inside the page render. Because the route ships aloading.tsx, the entire render is wrapped in a Suspense boundary. Next.js streams the loading skeleton first, then resolves the page. Whenredirect()ornotFound()fires after streaming has started, the public HTML response stays 200 and the body is the skeleton instead of a real 308 / 404.Combined effect:
/compare/A-vs-Band/compare/B-vs-A(and every ad-hoc invalid pair) all returned 200 with identical empty content. Pure URL duplication in the Google index.Fix
Two changes:
src/middleware.tsmatches/compare/:slug*and 308s any non-canonical<a>-vs-<b>slug to the alphabetical canonical, at the edge, before any rendering starts. Middleware fires before the loading.tsx Suspense boundary so the redirect actually goes out as a 308.src/app/compare/[slug]/page.tsxgenerateMetadatacallsnotFound()instead of returning{}when the pair fails to resolve, and runs the canonical redirect as a defensive backup.generateMetadataruns at the route layer (outside the Suspense), sonotFound()here correctly sets the not-found.tsx metadata (title + noindex) on the response.Validation
Local
pnpm buildplusnext start:/compare/arbitrum-vs-base(curated): 200 withArbitrum vs Base: live OpenChainBench benchmark data · OpenChainBench, self-canonical link, 1 h1./compare/base-vs-arbitrum(reverse curated): 308 to/compare/arbitrum-vs-base./compare/mobula-vs-coingecko(non-canonical ad-hoc): 308 to/compare/coingecko-vs-mobula./compare/helius-vs-quicknode(invalid, quicknode is not a provider): 200 withPage not found · OpenChainBenchtitle + noindex meta./compare/coingecko-vs-mobula(canonical ad-hoc, no shared benches): 200 withPage not found · OpenChainBenchtitle + noindex meta.All 22 curated pairs prerendered with unique title, self-canonical, and exactly 1 h1.