seo: route /pricing /stories /content, fix /help subpaths + locale trailing-slash dupes - #2682
seo: route /pricing /stories /content, fix /help subpaths + locale trailing-slash dupes#26820xkkonrad wants to merge 2 commits into
Conversation
…e trailing-slash dupes Four routing defects, all in the redirect/reserved-route layer: 1. /pricing, /stories and /content are real [locale]/(marketing) pages, but the bare paths are 7 lowercase letters, so couldBeRecipient() accepted them and the [...recipient] catch-all served a payment-profile shell on HTTP 200. Reserved in DEDICATED_ROUTES + 301'd to /en/.... 2. /help/delete-account (and every other help article at its bare path) soft-404'd the same way: only bare /help was redirected, never /help/:path*. That URL is the account-deletion link Google Play requires. Added /help/:path* and /stories/:path* so the newly-reserved prefixes keep resolving instead of hard-404ing. 3. skipTrailingSlashRedirect is on (it must stay — the PostHog /relay proxy is called with trailing slashes), so /en/help/ and /en/help both returned 200. Added one slash-stripping redirect scoped to the locale-prefixed tree only. 4. docs.peanut.to had no host rule; only docs.peanut.me did. 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.83 → 7158.96 (+0.13) 🆕 New findings (1)
✅ Resolved (1)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
Adversarial-review blocker: :path* matches zero segments, so bare locale homepages (/es-ar/, /es-419/, /pt-br/) compiled a 308 redirect to themselves - an infinite loop, cached permanently by browsers. Measured live on the preview; /en/ survived only because the /en -> / rule sits earlier in the array. :path+ requires a segment, so bare locale + slash falls through to its current 200 (matches production). Verified with next's checkCustomRoutes + compiled-rule replay of the loop paths and the intended redirect paths.
|
Re-probe after The review's blocker is gone: bare locale homepages no longer self-redirect, everything the rule was meant to do still works. Also validated offline with Next's own 🤖 Generated with Claude Code |
Part of the SEO code track (12 Aug 2026). Redirect/reserved-route layer only — no page or content changes. Sibling PRs in the same track touch
robots.ts/canonicals/corridors, the username catch-all'srobotsmeta, andsitemap.ts.Defects
1.
/pricing,/stories,/contentrender a payment profile, not the page they nameAll three ship as real pages under
src/app/[locale]/(marketing)/, but only at their locale-prefixed paths. The bare paths are seven lowercase letters, socouldBeRecipient()(src/constants/routes.ts) accepted them as usernames andsrc/app/[...recipient]/page.tsxserved a payment-profile shell — HTTP 200, wrong content, indexable.2.
/help/:path*soft-404s — including the Google Play account-deletion URLredirects.jsonredirected bare/helpbut never its subpaths, so every help article at its bare path fell into the same catch-all shell./help/delete-accountis the account-deletion URL required by Google Play's data-safety policy, and it returns a 200 payment shell today:3. Trailing-slash duplicates across the whole locale tree
skipTrailingSlashRedirect: true(next.config.js) turns off Next's global trailing-slash redirect, so/en/help/and/en/helpboth return 200 — two URLs, one page, for every marketing page in every locale.4.
docs.peanut.tohas no host rule.docs.peanut.mehas had one since the docs site was retired; the.totwin was never added. It is currently limping along on a Namecheap URL-forward that only works over plain HTTP —https://docs.peanut.to/times out, because Namecheap forwarding has no certificate for it:Fixes
redirects.json(+ three entries inDEDICATED_ROUTES, + a comment innext.config.js):/help/:path*/en/help/:path*/pricing/en/pricing/stories/en/stories/stories/:path*/en/stories/:path*/content/en/content/:path*(hostdocs.peanut.to)https://peanut.me/en/help/:locale(en|es-419|es-ar|pt-br)/:path*//:locale/:path*pricing,stories,contentadded toDEDICATED_ROUTESsoisReservedRoute()stops the catch-all claiming them./stories/:path*is here because/stories/[slug]exists: reservingstorieswithout it would turn live story URLs from a wrong-but-200 shell into a hard 404.Why
skipTrailingSlashRedirectstaysRemoving it is the obvious one-line fix for defect 3 and it is the wrong one. Next's built-in redirect is global, and the PostHog reverse proxy (
/relay/:path*rewrite, next.config.js) is called by the SDK with trailing slashes (/relay/decide/,/relay/e/) over POST. A 308 there either drops the body or costs every analytics event an extra round trip. The flag stays; the slash-stripping redirect is scoped to the fourSUPPORTED_LOCALESprefixes instead, so it structurally cannot reach/relay,/monitoring(Sentry tunnel),/passkeysor the recipient catch-all. A comment on the flag records this so the next person doesn't "simplify" it.308 vs 307
The new redirects are
permanent: true(Next emits 308, which Google treats as 301). These are duplicate/soft-404 URLs being consolidated, so signals should pass through. Bare/help,/terms,/privacykeep their existing 307 — untouched, to keep the hunk minimal. If locale negotiation ever lands on these paths, flippermanenttofalsefirst: 308s are cached by browsers indefinitely./privacy/:path*and/terms/:path*deliberately NOT addedBoth are single
page.tsxfiles with no[slug]child, so there is nothing under them to redirect. Adding a:path*rule would manufacture destinations that 404.Action required from the backend owner
storiesandcontentare still claimable as usernames. Frontend routing does not stop signup. Checked against the live username API (HEAD /users/username/{u}→ 200 taken / 400 reserved / 404 free):Please add
storiesandcontentto the server-side reserved-username list. Until then a user can registerstories, and this PR's redirect will quietly shadow their profile.Verification
next buildcannot run on the authoring box (earlyoom SIGTERMs it), so evidence is (a) Next's own route compiler run locally against the edited file and (b)curl -iagainst this PR's Vercel Deploy Preview.Local — Next's own redirect compiler
checkCustomRoutesis the validatornext buildruns before anything else;buildCustomRouteis the function that writesroutes-manifest.json, which is exactly what the Vercel proxy executes. Both were run against the editedredirects.jsonwith Next 16.2.3 from this repo's lockfile:Compiled regexes, evaluated in array order (first match wins), destination fed back in to catch loops:
Note the shape of every result: exactly one hop, no rule loops back onto its own source, and
/relay/*,/monitoring/,/api/og/,/passkeys/*match nothing.Preview —
curl -iCI
prettier --check,eslint,tsc --noEmit, jest and the Playwright e2e job all run on this PR — see the checks below.Conflicts to expect
host creator contest, base dev) adds'creator-contest'toDEDICATED_ROUTES. Same array, a few lines above this PR's block — trivial textual conflict, both additions are wanted.fix(seo): harden Split public content boundary, base main) rewrites the locale entries at the top ofredirects.json, adjacent to the/helpblock this PR edits. It reaches dev via back-merge on someone else's schedule; expect a conflict there and keep both sides.Caveats / follow-ups
/pressand/teamhave the identical defect (200 payment shell at the bare path, real page at/en/pressand/en/team). Both are already server-reserved (400from the username API), so the fix is safe — left out only to keep this PR's scope to the three routes it was scoped to. Straightforward follow-up: same two-line pattern.docs.peanut.torule is inert until ops moves the domain. It resolves to192.64.119.224(Namecheap), not Vercel — ahas: hostrule can only fire on requests that reach this project. Addingdocs.peanut.toas a domain on thepeanut-walletVercel project and repointing DNS activates it and fixes the HTTPS timeout above (Vercel issues the cert). The rule is landed now so the domain move should need no further deploy — but note this is untested until the domain is actually attached (review probed the preview with a spoofed Host header and a different Vercel project answered, so the rule couldn't be exercised end-to-end).docs.peanut.meis already on Vercel and 308s correctly today, which is the precedent this copies.SUPPORTED_LOCALES(src/i18n/types.ts). A fifth locale needs adding in both places; the comment onskipTrailingSlashRedirectsays so./en/deposit/via-avalanche/and friends still resolve in one hop — the existing/:locale/deposit/*rules sit above the new slash rule and already emit slashless destinations. Verified in the simulation above.🤖 Generated with Claude Code