seo: noindex the username catch-all route - #2678
Conversation
The [...recipient] catch-all renders an indexable "X on Peanut" shell for
ANY username-shaped string — existence is only checked client-side, so a
non-existent handle still returns HTTP 200 with full metadata. Google has
been indexing those as thin/soft-404 pages.
Set robots: { index: false, follow: false } on all four generateMetadata
return paths (reserved route, missing recipient, !couldBeRecipient, and the
full metadata object). No carve-outs: real profiles, ETH addresses, .eth ENS
names and request/receipt links are app surface, not search landing pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code-analysis diffPainscore total: 7158 → 7158.35 (+0.35) 🆕 New findings (7)
✅ Resolved (7)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
Adversarial-review catch: every branch of this route inherits
alternates.canonical '/' from the root layout, so the noindex was
shipping on pages that canonicalize to the homepage - a combination
Google's canonicalization guidance warns can bleed the noindex into
the cluster head (the homepage itself, sitemap priority 1.0).
alternates: { canonical: null } suppresses the inherited value; the
noindex pages now carry no canonical at all.
|
Adversarial review pass (agent) — HIGH finding, fixed in e46a576. The noindex was shipping alongside an inherited Two non-blocking review notes for the record:
🤖 Generated with Claude Code |
Defect (T3, layer 1)
src/app/[...recipient]/page.tsxis the username catch-all. ItsgenerateMetadatabuilds a full, indexable "X on Peanut" shell for any username-shaped string —USERNAME_PATTERN = /^[a-z][a-z0-9]{3,11}$/insrc/constants/routes.ts:155is the only filter. Whether the account exists is checked client-side, after the HTML has already been served with HTTP 200.Two consequences, both live on peanut.me right now:
So Google is offered an unbounded space of soft-404 "profile" pages, plus every real profile, address, ENS name and request/receipt link — all app surface, none of it a search landing page.
The
index, followcomes from the root layout (src/app/layout.tsx:24,robots: IS_PRODUCTION_DOMAIN ? { index: true, follow: true } : …). That is why the emptyreturn {}guard branches are not already safe: with nothing set at page level they inheritindex, followfrom the root.Fix
robots: { index: false, follow: false }on all fourgenerateMetadatareturn paths (count re-verified on currentorigin/dev@ad5b61b6—grep -n returnshows exactly 3 early returns plus the metadata object):!couldBeRecipient(...)Extracted as one
NOINDEXconst so the four branches cannot drift apart.Per the 12 Aug decision: noindex everything, no carve-outs — real profiles and
.ethENS pages included.One file, +11/−3. The page component, routing,
couldBeRecipient, and 404 behaviour are untouched.Verification
A/B on a local server, same worktree, only the patch differing. Run with
NEXT_PUBLIC_BASE_URL=https://peanut.me— without it the root layout noindexes every page locally and the evidence would be worthless.Before (
git checkout HEAD~1 -- src/app/[...recipient]/page.tsx):After (this branch):
/en/send-money-to/brazilis the control: a real content page, stillindex, followin both runs. The blast radius is the catch-all only./pricingmoving tonoindex, nofollowis the intended read of the decision — it is a catch-all shell today, not a real pricing page (isReservedRoute('/pricing')is false; the server-side username list rejects it, which is why nobody owns it). Giving it a real page is PR4's job, and that PR will set its own metadata.Gates:
CI on this branch:
typecheck,eslint,format,unit,e2e,analyze,Deploy-PreviewandVercelall green.Caveats
next buildwas never completed locally. The dev box has ~12 GB RAM shared with several concurrent agents; everynext build --webpackattempt was SIGTERM'd around 70 s at ~2.5 GB RSS with under 1.5 GB left free (seven attempts, including one withwebpackBuildWorker: falseto halve peak). Nothing in the failures referenced this diff — it never reached compile output. The production build is covered instead by the greenDeploy-Preview/Vercelchecks above.globalSetupneeds a live API on:5000with/dev/test-session, which is not available here. No new specs were written; the repo's existinge2ejob passes in CI.peanut.me, sosrc/app/layout.tsx:24noindexes every page there — including the Brazil control page. Only the local A/B above, withNEXT_PUBLIC_BASE_URL=https://peanut.me, isolates this change./enin both the before and after runs, while production serves it as a real page — a local locale-routing artifact, identical on either side of the patch, and untouched by this diff.loading.tsxremoval plus follow-up work.src/app/[...recipient]/(checked all 25 open PRs), so no conflict is expected.🤖 Generated with Claude Code