Uh oh!
There was an error while loading. Please reload this page.
fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy - #67
Conversation
…roxy
Registered projects' legacy id/UUID URLs (e.g. /projects/913449a2-…) were stuck
serving a cached soft-404 ("Proyecto no encontrado") instead of redirecting to
their canonical /projects/<slug>. Root cause: the single-segment page issues its
id→slug redirect with `permanentRedirect` INSIDE a Suspense boundary under
cacheComponents/PPR — that redirect degrades to a streamed meta tag and can't
overwrite a full-route cache entry previously cached as a 200 (the pre-registration
soft-404), so the URL is pinned. The legacy two-segment URLs already dodge this via
route handlers (real 308); the single-segment case is a page.
Fix: emit the redirect from a Next 16 `proxy.ts` (the renamed "middleware"
convention) — it runs BEFORE the full-route cache, so a real 308 can't be pinned,
and it uses only a small Upstash `id/old-slug → canonical-slug` map (never the
flaky relay/snapshot scans), making the redirect deterministic.
- lib/projectRedirectMap.ts (new): `buildRedirectMap(entries)` (pure) + an
edge-safe cached reader `resolveProjectRedirect(param)` (via upstashGet).
Keys = project id (lowercased) + old slugs; value = current canonical slug;
canonical-slug URLs are absent → pass through.
- proxy.ts (new): matches single-segment /projects/:param, 308s when the map has
a target; no-ops (NextResponse.next) for canonical slugs, unknown ids, or when
Upstash is unconfigured.
- lib/projectRegistry.ts: write the map on syncProjectRegistry + registerUserProjectSlug;
export refreshProjectRedirectMap().
- app/api/cache/warm/route.ts: refresh the map every cron so it stays warm.
Verified: tsc + build clean (proxy is edge-safe, no deprecation warning),
buildRedirectMap unit-tested, proxy no-ops safely without Upstash (next start:
/projects/* → 200). The Upstash-backed 308 + PPR cache-immunity verify on Vercel
(preview/prod have Upstash + PPR).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>The latest updates on your projects. Learn more about Vercel for GitHub.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds an Upstash-backed project redirect map, refreshes it during registry updates and cache warming, and introduces an edge proxy that redirects legacy ChangesProject redirect canonicalization
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Registry
participant Upstash
participant proxy
participant Client
Registry->>Upstash: Persist rebuilt redirect map
Client->>proxy: Request /projects/legacy-param
proxy->>Upstash: Read redirect map
Upstash-->>proxy: Return canonical slug
proxy-->>Client: 308 redirect to /projects/canonical-slug
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Uh oh!
There was an error while loading. Please reload this page.
Problem
Registered projects' legacy id/UUID URLs — e.g.
https://www.lacrypta.dev/projects/913449a2-ba7a-46fb-a599-7cc4d89e73ee— serve a cached soft-404 ("Proyecto no encontrado") to real users instead of redirecting to their canonical/projects/<slug>(which works fine, e.g./projects/ai-start). Confirmed systemic: every registered project's UUID URL is affected (pibot,ai-start, …), while unregistered projects' UUID URLs resolve normally.Root cause
The single-segment
/projects/[slug]page issues its id→slug redirect withpermanentRedirectinside a Suspense boundary undercacheComponents/PPR. That redirect degrades to a streamed meta tag and — critically — cannot overwrite a full-route cache entry previously cached as a 200 (the pre-registration soft-404). So once a project registers a slug, its old id URL is pinned to the stale soft-404 and never redirects. Neither PR #66's durable cache, nor cache warming, nor tag stale-marking/hard-expire un-sticks it (a forcedREVALIDATEDre-render still produced the soft-404).The codebase already documents this exact hazard on the two-segment legacy handlers (
app/projects/[slug]/[id]/route.ts): "A route handler (not a page) so the redirect is a real HTTP 308 — under cacheComponents a page-level redirect degrades to a streamed meta tag." The single-segment case is a page, so it can't be a route handler.Fix
Emit the redirect from a Next 16
proxy.ts(the renamedmiddlewarefile convention — build flagged the deprecation). The proxy runs before the full-route cache, so a real 308 can't be pinned, and it uses only a small Upstashid/old-slug → canonical-slugmap — never the flaky relay/snapshot scans — so the redirect is deterministic regardless of resolver/cache state.lib/projectRedirectMap.ts(new):buildRedirectMap(entries)(pure) + an edge-safe cached readerresolveProjectRedirect(param)(viaupstashGet, 60s in-memory cache). Keys = project id (lowercased) + any old slugs; value = current canonical slug (latest-wins per id). Canonical-slug URLs are absent from the map → pass through.proxy.ts(new): matches single-segment/projects/:param; 308s when the map has a target;NextResponse.next()for canonical slugs, unknown ids, or when Upstash is unconfigured.lib/projectRegistry.ts: writes the map onsyncProjectRegistry+registerUserProjectSlug; exportsrefreshProjectRedirectMap().app/api/cache/warm/route.ts: refreshes the map on the 5-min cron so it stays warm even without new registrations.Handles slug changes too: an old slug → the new canonical slug.
id === slug(curated projects) is excluded, so no self-redirect.Verification
tsc --noEmit+pnpm buildclean; proxy is edge-safe (bundled asƒ Proxy), no deprecation warning.buildRedirectMapunit-tested: id lowercasing, curatedid===slugexcluded, old-slug→new-slug, uuid→latest-canonical, canonical-slug-not-a-key.next start:/projects/*→ 200, no proxy/edge errors)./projects/913449a2-…should 308 → /projects/ai-start.Note
This does not retroactively purge the already-pinned soft-404 cache entries, but the proxy shadows them (runs first), so they stop being served. A one-time hard purge would need
REVALIDATE_SECRET(/api/revalidate-nostr) or a Vercel CDN purge.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
/projects/<id>links by routing them to the correct project page.