Problem
300+ /so-sanh/X-vs-Y URLs reported as 404 in Google Search Console this week. Root causes:
- Static export on Cloudflare Pages only serves API-curated pairs — unknown pairs 404
- Existing
SoSanh404Redirect is client-side JS only — Google sees HTTP 404, not a redirect
- Root-level
/X-vs-Y URLs (missing /so-sanh/ prefix) had no redirect handler
- 3-card comparison URLs (
/so-sanh/A-vs-B-vs-C) were broken due to indexOf('-vs-') only finding first match
Solution
Migrated from Cloudflare Pages static export to Vercel SSR.
app/(marketing)/so-sanh/[pair]/page.tsx
dynamicParams = true — unknown pairs render via SSR instead of 404
revalidate = 3600 — ISR caching for dynamically rendered pairs
- URL parsing changed from
indexOf('-vs-') to split('-vs-') — supports 2 or 3 card slugs
- Title renders all card names:
cards.map(c => c.name).join(' vs ')
next.config.ts
- Added server-side 301 redirect:
/:pair([^/]*-vs-[^/]*) → /so-sanh/:pair
- Handles root-level compare URLs, fires only on single-segment paths (no redirect loop)
.env.example
- Replaced real API key with placeholder
your-api-key-here
Result
- Curated pairs: pre-built static, fast, indexed ✓
- Unknown 2-card pairs: SSR on first request, cached 1h, indexable by Google ✓
- 3-card pairs (
/so-sanh/A-vs-B-vs-C): SSR, full 3-column comparison ✓
- Root-level
/X-vs-Y: server-side 301 → /so-sanh/X-vs-Y ✓
- Invalid pairs (card not found): real 404 ✓
Follow-up
Problem
300+
/so-sanh/X-vs-YURLs reported as 404 in Google Search Console this week. Root causes:SoSanh404Redirectis client-side JS only — Google sees HTTP 404, not a redirect/X-vs-YURLs (missing/so-sanh/prefix) had no redirect handler/so-sanh/A-vs-B-vs-C) were broken due toindexOf('-vs-')only finding first matchSolution
Migrated from Cloudflare Pages static export to Vercel SSR.
Changes (merged in ef56792, 1429b33)
app/(marketing)/so-sanh/[pair]/page.tsxdynamicParams = true— unknown pairs render via SSR instead of 404revalidate = 3600— ISR caching for dynamically rendered pairsindexOf('-vs-')tosplit('-vs-')— supports 2 or 3 card slugscards.map(c => c.name).join(' vs ')next.config.ts/:pair([^/]*-vs-[^/]*)→/so-sanh/:pair.env.exampleyour-api-key-hereResult
/so-sanh/A-vs-B-vs-C): SSR, full 3-column comparison ✓/X-vs-Y: server-side 301 →/so-sanh/X-vs-Y✓Follow-up