@decocms/nextjs 7.16.6 → 7.16.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -8
- package/package.json +3 -3
- package/src/createDecoPreviewPage.test.tsx +71 -0
- package/src/createDecoPreviewPage.tsx +108 -0
- package/src/index.ts +8 -3
- package/src/routeHandlers.test.ts +41 -9
- package/src/routeHandlers.ts +12 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# `@decocms/nextjs`
|
|
2
2
|
|
|
3
3
|
Deco framework binding for Next.js App Router — the Next.js sibling of
|
|
4
|
-
`@decocms/tanstack`.
|
|
4
|
+
`@decocms/tanstack`. Four surfaces, composed together:
|
|
5
5
|
|
|
6
6
|
- **`@decocms/nextjs/config`** — `withDeco(nextConfig)`, a `next.config`
|
|
7
7
|
wrapper that adds the rewrites and `transpilePackages` Deco's admin
|
|
@@ -9,12 +9,15 @@ Deco framework binding for Next.js App Router — the Next.js sibling of
|
|
|
9
9
|
- **`@decocms/nextjs/routeHandlers`** — `createDecoRouteHandlers({ setup })`,
|
|
10
10
|
a single catch-all dispatcher for the whole Studio admin protocol
|
|
11
11
|
(decofile read/reload, meta schema, invoke, live previews).
|
|
12
|
+
- **`@decocms/nextjs`** — `createDecoPreviewPage({ setup })`, an App Router
|
|
13
|
+
Server Component that renders live previews through Next's RSC pipeline,
|
|
14
|
+
including sections marked with `"use client"`.
|
|
12
15
|
- **`@decocms/nextjs/setup`** — `createNextSetup(options)`, a one-call site
|
|
13
16
|
bootstrap: the Next.js analogue of Vite's
|
|
14
17
|
`createSiteSetup` + `createAdminSetup` + `import.meta.glob`.
|
|
15
18
|
|
|
16
19
|
This document is the complete recipe a new Next.js site follows to wire all
|
|
17
|
-
|
|
20
|
+
all four together. Every code block below is meant to be copied, not
|
|
18
21
|
paraphrased.
|
|
19
22
|
|
|
20
23
|
## Install
|
|
@@ -78,13 +81,50 @@ import { ensureSetup } from "../../../deco/setup";
|
|
|
78
81
|
|
|
79
82
|
export const dynamic = "force-dynamic";
|
|
80
83
|
|
|
81
|
-
export const { GET, POST, OPTIONS } = createDecoRouteHandlers({
|
|
84
|
+
export const { GET, POST, OPTIONS } = createDecoRouteHandlers({
|
|
85
|
+
setup: ensureSetup,
|
|
86
|
+
});
|
|
82
87
|
```
|
|
83
88
|
|
|
84
89
|
`dynamic = "force-dynamic"` is required — this route must never be
|
|
85
90
|
statically cached (decofile reloads, invoke calls, and previews all need a
|
|
86
91
|
fresh response per request).
|
|
87
92
|
|
|
93
|
+
### The RSC preview page
|
|
94
|
+
|
|
95
|
+
Mount `createDecoPreviewPage` at the framework-owned `/deco/preview` path.
|
|
96
|
+
This path is intentionally not configurable: preview GET requests are always
|
|
97
|
+
redirected from the catch-all route to this App Router page, with the query
|
|
98
|
+
string preserved. Other admin requests, including `POST /deco/render`, remain
|
|
99
|
+
on the catch-all handler.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// src/app/deco/preview/[[...path]]/page.tsx
|
|
103
|
+
import { createDecoPreviewPage } from "@decocms/nextjs";
|
|
104
|
+
import { ensureSetup } from "../../../../deco/setup";
|
|
105
|
+
|
|
106
|
+
export const dynamic = "force-dynamic";
|
|
107
|
+
|
|
108
|
+
export default createDecoPreviewPage({ setup: ensureSetup });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The page boundary is essential for Client Components. A route handler can
|
|
112
|
+
produce plain HTML with `react-dom/server`, but that renderer cannot execute
|
|
113
|
+
the client-reference proxies Next creates for modules marked
|
|
114
|
+
`"use client"`. An App Router page is rendered by Next's React Server
|
|
115
|
+
Components pipeline, so it can render a server section tree containing
|
|
116
|
+
Client Components and preserve their hydration metadata.
|
|
117
|
+
|
|
118
|
+
Do not work around this error by removing `"use client"` from interactive
|
|
119
|
+
components. Keep sections and wrappers as Server Components when they only
|
|
120
|
+
render markup, but retain a Client Component boundary wherever hooks, event
|
|
121
|
+
handlers, browser APIs, or client-only context are required. The RSC preview
|
|
122
|
+
page exists so those two kinds of component can compose exactly as they do in
|
|
123
|
+
the storefront.
|
|
124
|
+
|
|
125
|
+
Importing the root barrel is correct in this `page.tsx`; the subpath-only
|
|
126
|
+
rule below applies specifically to `route.ts` files.
|
|
127
|
+
|
|
88
128
|
### Route-handler import rule: subpaths, never the root barrel
|
|
89
129
|
|
|
90
130
|
**Always import from `@decocms/nextjs/routeHandlers` (or `/config`,
|
|
@@ -189,7 +229,7 @@ successful bootstrap is cached for the life of the warm serverless
|
|
|
189
229
|
instance; a *rejected* bootstrap clears the memo so the next call retries
|
|
190
230
|
from scratch (the triggering call still rejects with the original error).
|
|
191
231
|
|
|
192
|
-
|
|
232
|
+
Three call sites need `ensureSetup()`:
|
|
193
233
|
|
|
194
234
|
1. **The catch-all route** (above) passes it as `{ setup: ensureSetup }` —
|
|
195
235
|
`createDecoRouteHandlers` awaits it before dispatching every admin
|
|
@@ -211,6 +251,9 @@ Two call sites need `ensureSetup()`:
|
|
|
211
251
|
|
|
212
252
|
(`app/layout.tsx` is a Server Component, not a route handler, so it's
|
|
213
253
|
fine to import the root barrel here — see the rule above.)
|
|
254
|
+
3. **The RSC preview page** passes it to `createDecoPreviewPage`, as shown
|
|
255
|
+
above. The setup function is memoized, so these call sites share one
|
|
256
|
+
successful bootstrap per warm process.
|
|
214
257
|
|
|
215
258
|
### `NextSetupOptions` at a glance
|
|
216
259
|
|
|
@@ -317,7 +360,8 @@ implementation file) can opt into these, each read as a literal
|
|
|
317
360
|
## Verifying your setup
|
|
318
361
|
|
|
319
362
|
`examples/nextjs-smoke` in this monorepo is a minimal, real Next.js App
|
|
320
|
-
Router build exercising all
|
|
321
|
-
the catch-all route, `createNextSetup`, and a resolved
|
|
322
|
-
|
|
323
|
-
|
|
363
|
+
Router build exercising all four surfaces end to end — `withDeco` rewrites,
|
|
364
|
+
the catch-all route, the RSC preview page, `createNextSetup`, and a resolved
|
|
365
|
+
page render. Its preview fixture is a real interactive Client Component, so
|
|
366
|
+
the build also guards the server/client boundary described above. Use it as
|
|
367
|
+
a working reference if any of the above doesn't compose the way you expect.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/nextjs",
|
|
3
|
-
"version": "7.16.
|
|
3
|
+
"version": "7.16.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"lint:unused": "knip"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@decocms/blocks": "7.16.
|
|
32
|
-
"@decocms/blocks-admin": "7.16.
|
|
31
|
+
"@decocms/blocks": "7.16.8",
|
|
32
|
+
"@decocms/blocks-admin": "7.16.8"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"next": ">=15.0.0",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { registerSection, setBlocks } from "@decocms/blocks/cms";
|
|
4
|
+
import { setRenderShell } from "@decocms/blocks-admin";
|
|
5
|
+
import { renderToString } from "react-dom/server";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { createDecoPreviewPage } from "./createDecoPreviewPage";
|
|
8
|
+
import * as nextjs from "./index";
|
|
9
|
+
|
|
10
|
+
vi.mock("next/headers", () => ({
|
|
11
|
+
headers: vi.fn(
|
|
12
|
+
async () =>
|
|
13
|
+
new Headers({
|
|
14
|
+
host: "store.test",
|
|
15
|
+
"x-forwarded-proto": "https",
|
|
16
|
+
"user-agent": "preview-test",
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const HERO = "site/sections/PreviewHero.tsx";
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
setBlocks({
|
|
25
|
+
"Preview Hero": { __resolveType: HERO, label: "rsc" },
|
|
26
|
+
});
|
|
27
|
+
registerSection(HERO, async () => ({
|
|
28
|
+
default: ({ label }: { label?: string }) => <h1>{`hero-${label}`}</h1>,
|
|
29
|
+
}));
|
|
30
|
+
setRenderShell({
|
|
31
|
+
css: "/preview.css",
|
|
32
|
+
fonts: ["/font.css"],
|
|
33
|
+
theme: "light",
|
|
34
|
+
bodyClass: "preview-body",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("createDecoPreviewPage", () => {
|
|
39
|
+
it("is exposed by the Next.js binding", () => {
|
|
40
|
+
expect((nextjs as Record<string, unknown>).createDecoPreviewPage).toBeTypeOf("function");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("resolves and renders a section through the RSC-aware SectionRenderer", async () => {
|
|
44
|
+
const setup = vi.fn(async () => {});
|
|
45
|
+
const Page = createDecoPreviewPage({ setup });
|
|
46
|
+
const element = await Page({
|
|
47
|
+
params: Promise.resolve({ path: ["Preview Hero"] }),
|
|
48
|
+
searchParams: Promise.resolve({ deviceHint: "desktop" }),
|
|
49
|
+
});
|
|
50
|
+
const html = renderToString(element);
|
|
51
|
+
|
|
52
|
+
expect(setup).toHaveBeenCalledOnce();
|
|
53
|
+
expect(html).toContain("hero-rsc");
|
|
54
|
+
expect(html).toContain('data-manifest-key="site/sections/PreviewHero.tsx"');
|
|
55
|
+
expect(html).toContain('href="/preview.css"');
|
|
56
|
+
expect(html).toContain('href="/font.css"');
|
|
57
|
+
expect(html).toContain('data-theme="light"');
|
|
58
|
+
expect(html).toContain("preview-body");
|
|
59
|
+
expect(html).toContain("editor::inject");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("renders a visible diagnostic for an unknown section", async () => {
|
|
63
|
+
const Page = createDecoPreviewPage();
|
|
64
|
+
const element = await Page({
|
|
65
|
+
params: Promise.resolve({ path: ["Missing"] }),
|
|
66
|
+
searchParams: Promise.resolve({}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(renderToString(element)).toContain("Unknown section: Missing");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { LIVE_CONTROLS_SCRIPT, resolvePreviewRequest } from "@decocms/blocks-admin";
|
|
2
|
+
import { getPreviewWrapper, getRenderShellConfig } from "@decocms/blocks-admin/admin/setup";
|
|
3
|
+
import { headers } from "next/headers";
|
|
4
|
+
import { cloneElement, createElement, type ReactElement, type ReactNode } from "react";
|
|
5
|
+
import { SectionRenderer } from "./SectionRenderer";
|
|
6
|
+
|
|
7
|
+
export interface CreateDecoPreviewPageOptions {
|
|
8
|
+
setup?: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DecoPreviewPageProps {
|
|
12
|
+
params: Promise<{ path?: string[] }>;
|
|
13
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function appendSearchParams(
|
|
17
|
+
target: URLSearchParams,
|
|
18
|
+
source: Record<string, string | string[] | undefined>,
|
|
19
|
+
) {
|
|
20
|
+
for (const [key, value] of Object.entries(source)) {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
for (const item of value) target.append(key, item);
|
|
23
|
+
} else if (value !== undefined) {
|
|
24
|
+
target.set(key, value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function buildPreviewRequest(props: DecoPreviewPageProps): Promise<Request> {
|
|
30
|
+
const [params, searchParams, requestHeaders] = await Promise.all([
|
|
31
|
+
props.params,
|
|
32
|
+
props.searchParams,
|
|
33
|
+
headers(),
|
|
34
|
+
]);
|
|
35
|
+
const host = requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host") ?? "localhost";
|
|
36
|
+
const protocol = requestHeaders.get("x-forwarded-proto") ?? "http";
|
|
37
|
+
const encodedPath = (params.path ?? []).map(encodeURIComponent).join("/");
|
|
38
|
+
const url = new URL(`/live/previews/${encodedPath}`, `${protocol}://${host}`);
|
|
39
|
+
appendSearchParams(url.searchParams, searchParams);
|
|
40
|
+
return new Request(url, { headers: new Headers(requestHeaders) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function PreviewResources() {
|
|
44
|
+
const { cssHref, fontHrefs } = getRenderShellConfig();
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
{fontHrefs.map((href) => (
|
|
48
|
+
<link key={href} rel="stylesheet" href={href} />
|
|
49
|
+
))}
|
|
50
|
+
{cssHref ? <link rel="stylesheet" href={cssHref} /> : null}
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function PreviewFrame({ children }: { children: ReactNode }) {
|
|
56
|
+
const { themeName, bodyClass } = getRenderShellConfig();
|
|
57
|
+
const Wrapper = getPreviewWrapper();
|
|
58
|
+
const content = Wrapper ? createElement(Wrapper, null, children) : children;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
<PreviewResources />
|
|
63
|
+
<div data-theme={themeName || "light"} className={bodyClass || undefined}>
|
|
64
|
+
{content}
|
|
65
|
+
</div>
|
|
66
|
+
<script dangerouslySetInnerHTML={{ __html: LIVE_CONTROLS_SCRIPT }} />
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function PreviewDiagnostic({ children }: { children: ReactNode }) {
|
|
72
|
+
return <div style={{ padding: 20, color: "red" }}>{children}</div>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createDecoPreviewPage(options: CreateDecoPreviewPageOptions = {}) {
|
|
76
|
+
return async function DecoPreviewPage(props: DecoPreviewPageProps) {
|
|
77
|
+
await options.setup?.();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const request = await buildPreviewRequest(props);
|
|
81
|
+
const resolution = await resolvePreviewRequest(request);
|
|
82
|
+
if (resolution.type === "unknown") {
|
|
83
|
+
return (
|
|
84
|
+
<PreviewFrame>
|
|
85
|
+
<PreviewDiagnostic>{`Unknown section: ${resolution.component}`}</PreviewDiagnostic>
|
|
86
|
+
</PreviewFrame>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rendered = await Promise.all(
|
|
91
|
+
resolution.sections.map(async (section, index): Promise<ReactElement | null> => {
|
|
92
|
+
const element = await SectionRenderer({ resolved: section });
|
|
93
|
+
return element
|
|
94
|
+
? cloneElement(element, { key: `${section.key}-${section.index ?? index}` })
|
|
95
|
+
: null;
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return <PreviewFrame>{rendered}</PreviewFrame>;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return (
|
|
102
|
+
<PreviewFrame>
|
|
103
|
+
<PreviewDiagnostic>{`Render error: ${(error as Error).message}`}</PreviewDiagnostic>
|
|
104
|
+
</PreviewFrame>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
export { ClientOnlySection } from "./ClientOnlySection";
|
|
1
2
|
export { createDecoPage } from "./createDecoPage";
|
|
2
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
type CreateDecoPreviewPageOptions,
|
|
5
|
+
createDecoPreviewPage,
|
|
6
|
+
type DecoPreviewPageProps,
|
|
7
|
+
} from "./createDecoPreviewPage";
|
|
3
8
|
export { DecoPageRenderer } from "./DecoPageRenderer";
|
|
4
|
-
export {
|
|
5
|
-
export { ClientOnlySection } from "./ClientOnlySection";
|
|
9
|
+
export { DecoRootLayout, type DecoRootLayoutProps } from "./DecoRootLayout";
|
|
6
10
|
export { DeferredSectionBoundary } from "./DeferredSection";
|
|
7
11
|
export {
|
|
8
12
|
decofileGET,
|
|
@@ -12,3 +16,4 @@ export {
|
|
|
12
16
|
renderGET,
|
|
13
17
|
renderPOST,
|
|
14
18
|
} from "./routeHandlers";
|
|
19
|
+
export { SectionRenderer } from "./SectionRenderer";
|
|
@@ -67,12 +67,44 @@ describe("createDecoRouteHandlers", () => {
|
|
|
67
67
|
expect(mocks.handleInvoke).toHaveBeenCalled();
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
it("
|
|
70
|
+
it("redirects preview GETs to the fixed RSC page and preserves the path and query", async () => {
|
|
71
71
|
const { GET } = createDecoRouteHandlers();
|
|
72
|
-
const res = await GET(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
const res = await GET(
|
|
73
|
+
new Request("http://x/deco/previews/pages-Home-123?props=x&deviceHint=mobile"),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(res.status).toBe(307);
|
|
77
|
+
expect(res.headers.get("location")).toBe(
|
|
78
|
+
"http://x/deco/preview/pages-Home-123?props=x&deviceHint=mobile",
|
|
79
|
+
);
|
|
80
|
+
expect(mocks.handleRender).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("redirects the public /live/previews path to the fixed RSC page", async () => {
|
|
84
|
+
const { GET } = createDecoRouteHandlers();
|
|
85
|
+
const res = await GET(
|
|
86
|
+
new Request("http://x/live/previews/site/sections/Hero.tsx?deviceHint=desktop"),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(res.status).toBe(307);
|
|
90
|
+
expect(res.headers.get("location")).toBe(
|
|
91
|
+
"http://x/deco/preview/site/sections/Hero.tsx?deviceHint=desktop",
|
|
92
|
+
);
|
|
93
|
+
expect(mocks.handleRender).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("keeps preview POSTs on handleRender", async () => {
|
|
97
|
+
const { POST } = createDecoRouteHandlers();
|
|
98
|
+
const res = await POST(
|
|
99
|
+
new Request("http://x/deco/previews/pages-Home-123", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify({ hello: "world" }),
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(res.status).toBe(200);
|
|
107
|
+
expect(mocks.handleRender).toHaveBeenCalledOnce();
|
|
76
108
|
});
|
|
77
109
|
|
|
78
110
|
it("forwards a POST body through the /deco/previews/* URL rebuild (Request-as-init carries the body stream)", async () => {
|
|
@@ -135,12 +167,12 @@ describe("createDecoRouteHandlers", () => {
|
|
|
135
167
|
expect(mocks.handleMeta).not.toHaveBeenCalled();
|
|
136
168
|
});
|
|
137
169
|
|
|
138
|
-
it("
|
|
170
|
+
it("redirects the rewrite-source /live/previews/* path to the fixed RSC page", async () => {
|
|
139
171
|
const { GET } = createDecoRouteHandlers();
|
|
140
172
|
const res = await GET(new Request("http://x/live/previews/pages-Home-123?props=x"));
|
|
141
|
-
expect(
|
|
142
|
-
|
|
143
|
-
expect(
|
|
173
|
+
expect(res.status).toBe(307);
|
|
174
|
+
expect(res.headers.get("location")).toBe("http://x/deco/preview/pages-Home-123?props=x");
|
|
175
|
+
expect(mocks.handleRender).not.toHaveBeenCalled();
|
|
144
176
|
});
|
|
145
177
|
|
|
146
178
|
it("405s GET /deco/invoke/* with Allow: POST (CSRF protection — see comment on the invoke branch)", async () => {
|
package/src/routeHandlers.ts
CHANGED
|
@@ -93,6 +93,8 @@ export interface DecoRouteHandlersOptions {
|
|
|
93
93
|
setup?: () => Promise<void>;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
const PREVIEW_PAGE_PATH = "/deco/preview";
|
|
97
|
+
|
|
96
98
|
/**
|
|
97
99
|
* Single catch-all dispatcher for the whole Studio admin protocol. Mount
|
|
98
100
|
* at `app/deco/[[...deco]]/route.ts` and wrap next.config with
|
|
@@ -104,7 +106,9 @@ export interface DecoRouteHandlersOptions {
|
|
|
104
106
|
* import { createDecoRouteHandlers } from "@decocms/nextjs/routeHandlers";
|
|
105
107
|
* import { ensureSetup } from "../../../deco/setup";
|
|
106
108
|
* export const dynamic = "force-dynamic";
|
|
107
|
-
* export const { GET, POST, OPTIONS } = createDecoRouteHandlers({
|
|
109
|
+
* export const { GET, POST, OPTIONS } = createDecoRouteHandlers({
|
|
110
|
+
* setup: ensureSetup,
|
|
111
|
+
* });
|
|
108
112
|
* ```
|
|
109
113
|
*
|
|
110
114
|
* `resolveAction` accepts BOTH the rewrite's public source path (e.g.
|
|
@@ -184,6 +188,13 @@ export function createDecoRouteHandlers(options: DecoRouteHandlersOptions = {}):
|
|
|
184
188
|
return handleInvoke(request);
|
|
185
189
|
}
|
|
186
190
|
if (action === "previews" || action.startsWith("previews/")) {
|
|
191
|
+
if (request.method === "GET") {
|
|
192
|
+
const rest = action === "previews" ? "" : action.slice("previews/".length);
|
|
193
|
+
const target = new URL(url);
|
|
194
|
+
target.pathname = rest ? `${PREVIEW_PAGE_PATH}/${rest}` : PREVIEW_PAGE_PATH;
|
|
195
|
+
return Response.redirect(target, 307);
|
|
196
|
+
}
|
|
197
|
+
|
|
187
198
|
// handleRender parses the literal "/live/previews/" prefix.
|
|
188
199
|
// `resolveAction` above already normalizes BOTH a direct
|
|
189
200
|
// `/deco/previews/*` hit and a rewritten `/live/previews/*` hit down
|