fix(compare): restore SSR content and canonical, 301 reverse-pair to alphabetical - #612
Merged
Merged
Conversation
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
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 (alphabetical ordering mismatch) or where either provider didn't resolve. With no per-page metadata, Next.js used the root layouttitle.defaultanddescription, so crawlers saw the homepage shell on every broken /compare URL.The page calls
redirect()(for non-canonical slugs) andnotFound()(for 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. Curl, Googlebot and any other non-JS client only see that skeleton.The combined effect:
/compare/A-vs-Band/compare/B-vs-A(and every ad-hoc invalid pair) all returned 200 with identical empty content and the homepage metadata. Pure URL duplication for indexing.Fix
Two changes:
src/middleware.tsmatches/compare/:slug*and 308s any non-canonical<a>-vs-<b>slug to the alphabetical canonical form, at the edge, before any rendering starts. This handles both reverse-pair URLs (base-vs-arbitrum->arbitrum-vs-base) and arbitrary input ordering (mobula-vs-coingecko->coingecko-vs-mobula). Middleware fires before the loading.tsx Suspense boundary so the redirect actually goes out as a 308.src/app/compare/[slug]/page.tsxgenerateMetadatanow runs the same gating as the page render: it callsnotFound()instead of returning{}when the pair fails to resolve, and it also 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, instead of falling through to the root layout defaults.The page render keeps its own
redirect()/notFound()calls as belt-and-suspenders.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.