From 821327bdadf7115fe60685649cc58976369083dc Mon Sep 17 00:00:00 2001 From: guitavano Date: Thu, 6 Aug 2026 12:39:45 -0300 Subject: [PATCH] fix(nextjs): actionable diagnostic when the RSC preview page is not mounted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A GET for /deco/preview/* reaches the catch-all route handler ONLY when app/deco/preview/[[...path]]/page.tsx is absent — with the page present, Next's static `preview` segment out-specifies the optional [[...deco]] catch-all and the handler never sees the request (verified against a real next build && next start, for both real-slash and %2F-encoded keys). Previously a missing page degraded to a cryptic `{"error":"Unknown deco route: /deco/preview/..."}` 404 — this is exactly how several migrated Next sites silently lost section previews. Return a 501 that names the missing file and how to create it instead. No behavior change for correctly-configured sites. Co-Authored-By: Claude Opus 4.8 --- packages/nextjs/src/routeHandlers.test.ts | 29 +++++++++++++++++++++++ packages/nextjs/src/routeHandlers.ts | 24 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/packages/nextjs/src/routeHandlers.test.ts b/packages/nextjs/src/routeHandlers.test.ts index 8d9d34bf..03e81d19 100644 --- a/packages/nextjs/src/routeHandlers.test.ts +++ b/packages/nextjs/src/routeHandlers.test.ts @@ -135,6 +135,35 @@ describe("createDecoRouteHandlers", () => { expect(res.status).toBe(404); }); + // A GET for the RSC preview PAGE (singular /deco/preview/*) reaches this + // route handler ONLY when app/deco/preview/[[...path]]/page.tsx is not + // mounted — with the page present, Next's static `preview` segment + // out-specifies the optional catch-all and this handler never runs. + // Missing page => actionable 501 diagnostic, not a cryptic "Unknown deco + // route" 404. Covers both the %2F-encoded single-segment key the studio + // sends and a decoded real-slash key. + it("501s /deco/preview/* (encoded key) with a mount-the-page diagnostic when the RSC page is absent", async () => { + const { GET } = createDecoRouteHandlers(); + const res = await GET(new Request("http://x/deco/preview/website%2Fpages%2FPage.tsx")); + expect(res.status).toBe(501); + const json = await res.json(); + expect(json.error).toMatch(/preview page not mounted/i); + expect(json.error).toMatch(/app\/deco\/preview\/\[\[\.\.\.path\]\]\/page\.tsx/); + expect(mocks.handleRender).not.toHaveBeenCalled(); + }); + + it("501s /deco/preview (no key) too", async () => { + const { GET } = createDecoRouteHandlers(); + const res = await GET(new Request("http://x/deco/preview")); + expect(res.status).toBe(501); + }); + + it("does not confuse /deco/previews (plural, the redirect) with the /deco/preview diagnostic", async () => { + const { GET } = createDecoRouteHandlers(); + const res = await GET(new Request("http://x/deco/previews/pages-Home-123")); + expect(res.status).toBe(307); + }); + // Regression coverage for the rewrite-source paths, not just their // /deco/* destinations: a Next.js App Router route handler reached via a // next.config.js `rewrites()` entry sees `request.url` as the ORIGINAL, diff --git a/packages/nextjs/src/routeHandlers.ts b/packages/nextjs/src/routeHandlers.ts index bca2c0c9..99d2f442 100644 --- a/packages/nextjs/src/routeHandlers.ts +++ b/packages/nextjs/src/routeHandlers.ts @@ -213,6 +213,30 @@ export function createDecoRouteHandlers(options: DecoRouteHandlersOptions = {}): // JSON body through this branch and asserts it still parses. return handleRender(new Request(rebuilt, request)); } + if (action === "preview" || action.startsWith("preview/")) { + // A request for the RSC preview PAGE (singular `preview`, the target of + // the `previews` redirect above) that reached THIS route handler means + // `app/deco/preview/[[...path]]/page.tsx` is not mounted on the site. + // When that page exists, Next routes `/deco/preview/*` to it — its + // static `preview` segment out-specifies this optional catch-all, so + // the handler never sees the request (verified against a real + // `next build && next start`, for both real-slash and %2F-encoded + // keys). A missing page otherwise degrades to a cryptic + // "Unknown deco route" 404; return an actionable diagnostic instead so + // the misconfiguration is self-explanatory. Do NOT try to render the + // preview here via `handleRender`: this handler runs in the + // react-server module graph, where `renderToString` cannot invoke the + // client-reference proxies Next creates for `"use client"` components — + // which is the entire reason the dedicated RSC page exists. + return new Response( + JSON.stringify({ + error: + "Deco preview page not mounted. Add app/deco/preview/[[...path]]/page.tsx exporting `createDecoPreviewPage({ setup })` from @decocms/nextjs (see the deco-next-package-migration skill's admin-routes template).", + path: url.pathname, + }), + { status: 501, headers: { "Content-Type": "application/json" } }, + ); + } return new Response(JSON.stringify({ error: `Unknown deco route: ${url.pathname}` }), { status: 404, headers: { "Content-Type": "application/json" },