fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy - #67

Merged
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware
Jul 17, 2026
Merged

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy#67
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 17, 2026

Copy link
Copy Markdown
Member

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 with permanentRedirectinside a Suspense boundary under cacheComponents/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 forced REVALIDATED re-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 renamed middleware file 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 Upstash id/old-slug → canonical-slug map — 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 reader resolveProjectRedirect(param) (via upstashGet, 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 on syncProjectRegistry + registerUserProjectSlug; exports refreshProjectRedirectMap().
  • 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 build clean; proxy is edge-safe (bundled as ƒ Proxy), no deprecation warning.
  • buildRedirectMap unit-tested: id lowercasing, curated id===slug excluded, old-slug→new-slug, uuid→latest-canonical, canonical-slug-not-a-key.
  • Proxy no-ops safely without Upstash (next start: /projects/* → 200, no proxy/edge errors).
  • ⏳ The Upstash-backed 308 and PPR cache-immunity verify on Vercel (preview/prod have Upstash + PPR; local Docker/Upstash was unavailable). After deploy, /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

    • Added automatic redirects from legacy project IDs and previous slugs to each project’s current canonical URL.
    • Redirects preserve navigation when project registrations are unavailable.
    • Added cache warming support to keep project redirects up to date.
  • Bug Fixes

    • Improved handling of outdated /projects/<id> links by routing them to the correct project page.
    • Redirect lookup failures no longer interrupt normal navigation.

…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>
@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 17, 2026 9:26pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3af21179-3138-4b6f-b114-6875d37e81f1

📥 Commits

Reviewing files that changed from the base of the PR and between 252be97 and 9780778.

📒 Files selected for processing (4)
  • app/api/cache/warm/route.ts
  • lib/projectRedirectMap.ts
  • lib/projectRegistry.ts
  • proxy.ts

📝 Walkthrough

Walkthrough

Adds an Upstash-backed project redirect map, refreshes it during registry updates and cache warming, and introduces an edge proxy that redirects legacy /projects/<param> URLs to canonical project slugs.

Changes

Project redirect canonicalization

Layer / File(s)Summary
Redirect map construction and lookup
lib/projectRedirectMap.ts
Builds canonical and legacy-slug mappings, persists lookup results through Upstash, caches reads for 60 seconds, and safely resolves normalized parameters.
Registry and cache-warming refresh
lib/projectRegistry.ts, app/api/cache/warm/route.ts
Refreshes the redirect map after registry synchronization, slug registration, and cache-warming execution.
Edge project URL canonicalization
proxy.ts
Matches single-segment project paths, resolves redirect mappings, returns 308 redirects for legacy identifiers, and otherwise continues the request.

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
Loading

Possibly related PRs

  • lacrypta/lacrypta-dev#64: Both changes modify the cache-warming endpoint; this PR adds redirect-map refreshing to that flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding proxy-based 308 redirects for legacy project URLs to canonical slugs.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/project-id-redirect-middleware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit fd8ce31 into mainJul 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy - #67

Merged
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware
Jul 17, 2026
Merged

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy#67
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 17, 2026

Copy link
Copy Markdown
Member

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 with permanentRedirectinside a Suspense boundary under cacheComponents/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 forced REVALIDATED re-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 renamed middleware file 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 Upstash id/old-slug → canonical-slug map — 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 reader resolveProjectRedirect(param) (via upstashGet, 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 on syncProjectRegistry + registerUserProjectSlug; exports refreshProjectRedirectMap().
  • 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 build clean; proxy is edge-safe (bundled as ƒ Proxy), no deprecation warning.
  • buildRedirectMap unit-tested: id lowercasing, curated id===slug excluded, old-slug→new-slug, uuid→latest-canonical, canonical-slug-not-a-key.
  • Proxy no-ops safely without Upstash (next start: /projects/* → 200, no proxy/edge errors).
  • ⏳ The Upstash-backed 308 and PPR cache-immunity verify on Vercel (preview/prod have Upstash + PPR; local Docker/Upstash was unavailable). After deploy, /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

    • Added automatic redirects from legacy project IDs and previous slugs to each project’s current canonical URL.
    • Redirects preserve navigation when project registrations are unavailable.
    • Added cache warming support to keep project redirects up to date.
  • Bug Fixes

    • Improved handling of outdated /projects/<id> links by routing them to the correct project page.
    • Redirect lookup failures no longer interrupt normal navigation.

…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>
@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 17, 2026 9:26pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3af21179-3138-4b6f-b114-6875d37e81f1

📥 Commits

Reviewing files that changed from the base of the PR and between 252be97 and 9780778.

📒 Files selected for processing (4)
  • app/api/cache/warm/route.ts
  • lib/projectRedirectMap.ts
  • lib/projectRegistry.ts
  • proxy.ts

📝 Walkthrough

Walkthrough

Adds an Upstash-backed project redirect map, refreshes it during registry updates and cache warming, and introduces an edge proxy that redirects legacy /projects/<param> URLs to canonical project slugs.

Changes

Project redirect canonicalization

Layer / File(s)Summary
Redirect map construction and lookup
lib/projectRedirectMap.ts
Builds canonical and legacy-slug mappings, persists lookup results through Upstash, caches reads for 60 seconds, and safely resolves normalized parameters.
Registry and cache-warming refresh
lib/projectRegistry.ts, app/api/cache/warm/route.ts
Refreshes the redirect map after registry synchronization, slug registration, and cache-warming execution.
Edge project URL canonicalization
proxy.ts
Matches single-segment project paths, resolves redirect mappings, returns 308 redirects for legacy identifiers, and otherwise continues the request.

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
Loading

Possibly related PRs

  • lacrypta/lacrypta-dev#64: Both changes modify the cache-warming endpoint; this PR adds redirect-map refreshing to that flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding proxy-based 308 redirects for legacy project URLs to canonical slugs.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/project-id-redirect-middleware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit fd8ce31 into mainJul 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy - #67

Merged
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware
Jul 17, 2026
Merged

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy#67
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 17, 2026

Copy link
Copy Markdown
Member

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 with permanentRedirectinside a Suspense boundary under cacheComponents/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 forced REVALIDATED re-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 renamed middleware file 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 Upstash id/old-slug → canonical-slug map — 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 reader resolveProjectRedirect(param) (via upstashGet, 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 on syncProjectRegistry + registerUserProjectSlug; exports refreshProjectRedirectMap().
  • 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 build clean; proxy is edge-safe (bundled as ƒ Proxy), no deprecation warning.
  • buildRedirectMap unit-tested: id lowercasing, curated id===slug excluded, old-slug→new-slug, uuid→latest-canonical, canonical-slug-not-a-key.
  • Proxy no-ops safely without Upstash (next start: /projects/* → 200, no proxy/edge errors).
  • ⏳ The Upstash-backed 308 and PPR cache-immunity verify on Vercel (preview/prod have Upstash + PPR; local Docker/Upstash was unavailable). After deploy, /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

    • Added automatic redirects from legacy project IDs and previous slugs to each project’s current canonical URL.
    • Redirects preserve navigation when project registrations are unavailable.
    • Added cache warming support to keep project redirects up to date.
  • Bug Fixes

    • Improved handling of outdated /projects/<id> links by routing them to the correct project page.
    • Redirect lookup failures no longer interrupt normal navigation.

…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>
@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 17, 2026 9:26pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3af21179-3138-4b6f-b114-6875d37e81f1

📥 Commits

Reviewing files that changed from the base of the PR and between 252be97 and 9780778.

📒 Files selected for processing (4)
  • app/api/cache/warm/route.ts
  • lib/projectRedirectMap.ts
  • lib/projectRegistry.ts
  • proxy.ts

📝 Walkthrough

Walkthrough

Adds an Upstash-backed project redirect map, refreshes it during registry updates and cache warming, and introduces an edge proxy that redirects legacy /projects/<param> URLs to canonical project slugs.

Changes

Project redirect canonicalization

Layer / File(s)Summary
Redirect map construction and lookup
lib/projectRedirectMap.ts
Builds canonical and legacy-slug mappings, persists lookup results through Upstash, caches reads for 60 seconds, and safely resolves normalized parameters.
Registry and cache-warming refresh
lib/projectRegistry.ts, app/api/cache/warm/route.ts
Refreshes the redirect map after registry synchronization, slug registration, and cache-warming execution.
Edge project URL canonicalization
proxy.ts
Matches single-segment project paths, resolves redirect mappings, returns 308 redirects for legacy identifiers, and otherwise continues the request.

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
Loading

Possibly related PRs

  • lacrypta/lacrypta-dev#64: Both changes modify the cache-warming endpoint; this PR adds redirect-map refreshing to that flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding proxy-based 308 redirects for legacy project URLs to canonical slugs.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/project-id-redirect-middleware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit fd8ce31 into mainJul 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy - #67

Merged
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware
Jul 17, 2026
Merged

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy#67
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 17, 2026

Copy link
Copy Markdown
Member

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 with permanentRedirectinside a Suspense boundary under cacheComponents/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 forced REVALIDATED re-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 renamed middleware file 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 Upstash id/old-slug → canonical-slug map — 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 reader resolveProjectRedirect(param) (via upstashGet, 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 on syncProjectRegistry + registerUserProjectSlug; exports refreshProjectRedirectMap().
  • 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 build clean; proxy is edge-safe (bundled as ƒ Proxy), no deprecation warning.
  • buildRedirectMap unit-tested: id lowercasing, curated id===slug excluded, old-slug→new-slug, uuid→latest-canonical, canonical-slug-not-a-key.
  • Proxy no-ops safely without Upstash (next start: /projects/* → 200, no proxy/edge errors).
  • ⏳ The Upstash-backed 308 and PPR cache-immunity verify on Vercel (preview/prod have Upstash + PPR; local Docker/Upstash was unavailable). After deploy, /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

    • Added automatic redirects from legacy project IDs and previous slugs to each project’s current canonical URL.
    • Redirects preserve navigation when project registrations are unavailable.
    • Added cache warming support to keep project redirects up to date.
  • Bug Fixes

    • Improved handling of outdated /projects/<id> links by routing them to the correct project page.
    • Redirect lookup failures no longer interrupt normal navigation.

…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>
@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 17, 2026 9:26pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3af21179-3138-4b6f-b114-6875d37e81f1

📥 Commits

Reviewing files that changed from the base of the PR and between 252be97 and 9780778.

📒 Files selected for processing (4)
  • app/api/cache/warm/route.ts
  • lib/projectRedirectMap.ts
  • lib/projectRegistry.ts
  • proxy.ts

📝 Walkthrough

Walkthrough

Adds an Upstash-backed project redirect map, refreshes it during registry updates and cache warming, and introduces an edge proxy that redirects legacy /projects/<param> URLs to canonical project slugs.

Changes

Project redirect canonicalization

Layer / File(s)Summary
Redirect map construction and lookup
lib/projectRedirectMap.ts
Builds canonical and legacy-slug mappings, persists lookup results through Upstash, caches reads for 60 seconds, and safely resolves normalized parameters.
Registry and cache-warming refresh
lib/projectRegistry.ts, app/api/cache/warm/route.ts
Refreshes the redirect map after registry synchronization, slug registration, and cache-warming execution.
Edge project URL canonicalization
proxy.ts
Matches single-segment project paths, resolves redirect mappings, returns 308 redirects for legacy identifiers, and otherwise continues the request.

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
Loading

Possibly related PRs

  • lacrypta/lacrypta-dev#64: Both changes modify the cache-warming endpoint; this PR adds redirect-map refreshing to that flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding proxy-based 308 redirects for legacy project URLs to canonical slugs.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/project-id-redirect-middleware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit fd8ce31 into mainJul 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy - #67

Merged
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware
Jul 17, 2026
Merged

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy#67
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 17, 2026

Copy link
Copy Markdown
Member

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 with permanentRedirectinside a Suspense boundary under cacheComponents/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 forced REVALIDATED re-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 renamed middleware file 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 Upstash id/old-slug → canonical-slug map — 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 reader resolveProjectRedirect(param) (via upstashGet, 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 on syncProjectRegistry + registerUserProjectSlug; exports refreshProjectRedirectMap().
  • 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 build clean; proxy is edge-safe (bundled as ƒ Proxy), no deprecation warning.
  • buildRedirectMap unit-tested: id lowercasing, curated id===slug excluded, old-slug→new-slug, uuid→latest-canonical, canonical-slug-not-a-key.
  • Proxy no-ops safely without Upstash (next start: /projects/* → 200, no proxy/edge errors).
  • ⏳ The Upstash-backed 308 and PPR cache-immunity verify on Vercel (preview/prod have Upstash + PPR; local Docker/Upstash was unavailable). After deploy, /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

    • Added automatic redirects from legacy project IDs and previous slugs to each project’s current canonical URL.
    • Redirects preserve navigation when project registrations are unavailable.
    • Added cache warming support to keep project redirects up to date.
  • Bug Fixes

    • Improved handling of outdated /projects/<id> links by routing them to the correct project page.
    • Redirect lookup failures no longer interrupt normal navigation.

…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>
@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 17, 2026 9:26pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3af21179-3138-4b6f-b114-6875d37e81f1

📥 Commits

Reviewing files that changed from the base of the PR and between 252be97 and 9780778.

📒 Files selected for processing (4)
  • app/api/cache/warm/route.ts
  • lib/projectRedirectMap.ts
  • lib/projectRegistry.ts
  • proxy.ts

📝 Walkthrough

Walkthrough

Adds an Upstash-backed project redirect map, refreshes it during registry updates and cache warming, and introduces an edge proxy that redirects legacy /projects/<param> URLs to canonical project slugs.

Changes

Project redirect canonicalization

Layer / File(s)Summary
Redirect map construction and lookup
lib/projectRedirectMap.ts
Builds canonical and legacy-slug mappings, persists lookup results through Upstash, caches reads for 60 seconds, and safely resolves normalized parameters.
Registry and cache-warming refresh
lib/projectRegistry.ts, app/api/cache/warm/route.ts
Refreshes the redirect map after registry synchronization, slug registration, and cache-warming execution.
Edge project URL canonicalization
proxy.ts
Matches single-segment project paths, resolves redirect mappings, returns 308 redirects for legacy identifiers, and otherwise continues the request.

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
Loading

Possibly related PRs

  • lacrypta/lacrypta-dev#64: Both changes modify the cache-warming endpoint; this PR adds redirect-map refreshing to that flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding proxy-based 308 redirects for legacy project URLs to canonical slugs.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/project-id-redirect-middleware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit fd8ce31 into mainJul 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy - #67

Merged
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware
Jul 17, 2026
Merged

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy#67
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 17, 2026

Copy link
Copy Markdown
Member

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 with permanentRedirectinside a Suspense boundary under cacheComponents/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 forced REVALIDATED re-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 renamed middleware file 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 Upstash id/old-slug → canonical-slug map — 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 reader resolveProjectRedirect(param) (via upstashGet, 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 on syncProjectRegistry + registerUserProjectSlug; exports refreshProjectRedirectMap().
  • 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 build clean; proxy is edge-safe (bundled as ƒ Proxy), no deprecation warning.
  • buildRedirectMap unit-tested: id lowercasing, curated id===slug excluded, old-slug→new-slug, uuid→latest-canonical, canonical-slug-not-a-key.
  • Proxy no-ops safely without Upstash (next start: /projects/* → 200, no proxy/edge errors).
  • ⏳ The Upstash-backed 308 and PPR cache-immunity verify on Vercel (preview/prod have Upstash + PPR; local Docker/Upstash was unavailable). After deploy, /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

    • Added automatic redirects from legacy project IDs and previous slugs to each project’s current canonical URL.
    • Redirects preserve navigation when project registrations are unavailable.
    • Added cache warming support to keep project redirects up to date.
  • Bug Fixes

    • Improved handling of outdated /projects/<id> links by routing them to the correct project page.
    • Redirect lookup failures no longer interrupt normal navigation.

…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>
@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 17, 2026 9:26pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3af21179-3138-4b6f-b114-6875d37e81f1

📥 Commits

Reviewing files that changed from the base of the PR and between 252be97 and 9780778.

📒 Files selected for processing (4)
  • app/api/cache/warm/route.ts
  • lib/projectRedirectMap.ts
  • lib/projectRegistry.ts
  • proxy.ts

📝 Walkthrough

Walkthrough

Adds an Upstash-backed project redirect map, refreshes it during registry updates and cache warming, and introduces an edge proxy that redirects legacy /projects/<param> URLs to canonical project slugs.

Changes

Project redirect canonicalization

Layer / File(s)Summary
Redirect map construction and lookup
lib/projectRedirectMap.ts
Builds canonical and legacy-slug mappings, persists lookup results through Upstash, caches reads for 60 seconds, and safely resolves normalized parameters.
Registry and cache-warming refresh
lib/projectRegistry.ts, app/api/cache/warm/route.ts
Refreshes the redirect map after registry synchronization, slug registration, and cache-warming execution.
Edge project URL canonicalization
proxy.ts
Matches single-segment project paths, resolves redirect mappings, returns 308 redirects for legacy identifiers, and otherwise continues the request.

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
Loading

Possibly related PRs

  • lacrypta/lacrypta-dev#64: Both changes modify the cache-warming endpoint; this PR adds redirect-map refreshing to that flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding proxy-based 308 redirects for legacy project URLs to canonical slugs.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/project-id-redirect-middleware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit fd8ce31 into mainJul 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy - #67

Merged
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware
Jul 17, 2026
Merged

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy#67
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 17, 2026

Copy link
Copy Markdown
Member

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 with permanentRedirectinside a Suspense boundary under cacheComponents/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 forced REVALIDATED re-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 renamed middleware file 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 Upstash id/old-slug → canonical-slug map — 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 reader resolveProjectRedirect(param) (via upstashGet, 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 on syncProjectRegistry + registerUserProjectSlug; exports refreshProjectRedirectMap().
  • 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 build clean; proxy is edge-safe (bundled as ƒ Proxy), no deprecation warning.
  • buildRedirectMap unit-tested: id lowercasing, curated id===slug excluded, old-slug→new-slug, uuid→latest-canonical, canonical-slug-not-a-key.
  • Proxy no-ops safely without Upstash (next start: /projects/* → 200, no proxy/edge errors).
  • ⏳ The Upstash-backed 308 and PPR cache-immunity verify on Vercel (preview/prod have Upstash + PPR; local Docker/Upstash was unavailable). After deploy, /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

    • Added automatic redirects from legacy project IDs and previous slugs to each project’s current canonical URL.
    • Redirects preserve navigation when project registrations are unavailable.
    • Added cache warming support to keep project redirects up to date.
  • Bug Fixes

    • Improved handling of outdated /projects/<id> links by routing them to the correct project page.
    • Redirect lookup failures no longer interrupt normal navigation.

…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>
@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 17, 2026 9:26pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3af21179-3138-4b6f-b114-6875d37e81f1

📥 Commits

Reviewing files that changed from the base of the PR and between 252be97 and 9780778.

📒 Files selected for processing (4)
  • app/api/cache/warm/route.ts
  • lib/projectRedirectMap.ts
  • lib/projectRegistry.ts
  • proxy.ts

📝 Walkthrough

Walkthrough

Adds an Upstash-backed project redirect map, refreshes it during registry updates and cache warming, and introduces an edge proxy that redirects legacy /projects/<param> URLs to canonical project slugs.

Changes

Project redirect canonicalization

Layer / File(s)Summary
Redirect map construction and lookup
lib/projectRedirectMap.ts
Builds canonical and legacy-slug mappings, persists lookup results through Upstash, caches reads for 60 seconds, and safely resolves normalized parameters.
Registry and cache-warming refresh
lib/projectRegistry.ts, app/api/cache/warm/route.ts
Refreshes the redirect map after registry synchronization, slug registration, and cache-warming execution.
Edge project URL canonicalization
proxy.ts
Matches single-segment project paths, resolves redirect mappings, returns 308 redirects for legacy identifiers, and otherwise continues the request.

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
Loading

Possibly related PRs

  • lacrypta/lacrypta-dev#64: Both changes modify the cache-warming endpoint; this PR adds redirect-map refreshing to that flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding proxy-based 308 redirects for legacy project URLs to canonical slugs.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/project-id-redirect-middleware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit fd8ce31 into mainJul 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@agustinkassis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy - #67

Merged
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware
Jul 17, 2026
Merged

fix(projects): 308 legacy /projects/<id> URLs to canonical slug via proxy#67
agustinkassis merged 1 commit into
mainfrom
claude/project-id-redirect-middleware

Conversation

@agustinkassis

@agustinkassisagustinkassis commented Jul 17, 2026

Copy link
Copy Markdown
Member

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 with permanentRedirectinside a Suspense boundary under cacheComponents/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 forced REVALIDATED re-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 renamed middleware file 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 Upstash id/old-slug → canonical-slug map — 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 reader resolveProjectRedirect(param) (via upstashGet, 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 on syncProjectRegistry + registerUserProjectSlug; exports refreshProjectRedirectMap().
  • 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 build clean; proxy is edge-safe (bundled as ƒ Proxy), no deprecation warning.
  • buildRedirectMap unit-tested: id lowercasing, curated id===slug excluded, old-slug→new-slug, uuid→latest-canonical, canonical-slug-not-a-key.
  • Proxy no-ops safely without Upstash (next start: /projects/* → 200, no proxy/edge errors).
  • ⏳ The Upstash-backed 308 and PPR cache-immunity verify on Vercel (preview/prod have Upstash + PPR; local Docker/Upstash was unavailable). After deploy, /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

    • Added automatic redirects from legacy project IDs and previous slugs to each project’s current canonical URL.
    • Redirects preserve navigation when project registrations are unavailable.
    • Added cache warming support to keep project redirects up to date.
  • Bug Fixes

    • Improved handling of outdated /projects/<id> links by routing them to the correct project page.
    • Redirect lookup failures no longer interrupt normal navigation.

…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>
@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
lacrypta-devReadyReadyPreview, CommentJul 17, 2026 9:26pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3af21179-3138-4b6f-b114-6875d37e81f1

📥 Commits

Reviewing files that changed from the base of the PR and between 252be97 and 9780778.

📒 Files selected for processing (4)
  • app/api/cache/warm/route.ts
  • lib/projectRedirectMap.ts
  • lib/projectRegistry.ts
  • proxy.ts

📝 Walkthrough

Walkthrough

Adds an Upstash-backed project redirect map, refreshes it during registry updates and cache warming, and introduces an edge proxy that redirects legacy /projects/<param> URLs to canonical project slugs.

Changes

Project redirect canonicalization

Layer / File(s)Summary
Redirect map construction and lookup
lib/projectRedirectMap.ts
Builds canonical and legacy-slug mappings, persists lookup results through Upstash, caches reads for 60 seconds, and safely resolves normalized parameters.
Registry and cache-warming refresh
lib/projectRegistry.ts, app/api/cache/warm/route.ts
Refreshes the redirect map after registry synchronization, slug registration, and cache-warming execution.
Edge project URL canonicalization
proxy.ts
Matches single-segment project paths, resolves redirect mappings, returns 308 redirects for legacy identifiers, and otherwise continues the request.

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
Loading

Possibly related PRs

  • lacrypta/lacrypta-dev#64: Both changes modify the cache-warming endpoint; this PR adds redirect-map refreshing to that flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding proxy-based 308 redirects for legacy project URLs to canonical slugs.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/project-id-redirect-middleware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agustinkassis
agustinkassis merged commit fd8ce31 into mainJul 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@agustinkassis