Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/nextjs/src/routeHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions packages/nextjs/src/routeHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down