@decocms/nextjs 7.28.0-beta.0 → 7.28.0-beta.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/nextjs",
3
- "version": "7.28.0-beta.0",
3
+ "version": "7.28.0-beta.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -29,8 +29,8 @@
29
29
  "lint:unused": "knip"
30
30
  },
31
31
  "dependencies": {
32
- "@decocms/blocks": "7.28.0-beta.0",
33
- "@decocms/blocks-admin": "7.28.0-beta.0"
32
+ "@decocms/blocks": "7.28.0-beta.1",
33
+ "@decocms/blocks-admin": "7.28.0-beta.1"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "next": ">=15.0.0",
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { describe, expect, it } from "vitest";
3
3
 
4
4
  import { DRAFT_COOKIE } from "./draft";
5
- import { applyDraft, prepareDraft } from "./draftMiddleware";
5
+ import { applyDraft, prepareDraft, rewriteToDraftRoute } from "./draftMiddleware";
6
6
 
7
7
  function request(url: string, cookie?: string): NextRequest {
8
8
  const req = new NextRequest(new URL(url), {
@@ -79,3 +79,39 @@ describe("applyDraft", () => {
79
79
  expect(res.headers.get("cache-control")).toBeNull();
80
80
  });
81
81
  });
82
+
83
+ describe("rewriteToDraftRoute", () => {
84
+ it("rewrites a drafted request onto the dynamic draft route", () => {
85
+ const req = request("https://site.example/blog/hello?__draft=abc@v1");
86
+ const res = rewriteToDraftRoute(req, prepareDraft(req));
87
+ const target = new URL(res?.headers.get("x-middleware-rewrite") ?? "");
88
+ expect(target.pathname).toBe("/_draft/blog/hello");
89
+ // The pointer must survive: the draft page reads it from searchParams.
90
+ expect(target.searchParams.get("__draft")).toBe("abc@v1");
91
+ });
92
+
93
+ it("rewrites a cookie-only navigation too", () => {
94
+ const req = request("https://site.example/blog/hello", "abc@v1");
95
+ const res = rewriteToDraftRoute(req, prepareDraft(req));
96
+ expect(new URL(res?.headers.get("x-middleware-rewrite") ?? "").pathname).toBe(
97
+ "/_draft/blog/hello",
98
+ );
99
+ });
100
+
101
+ it("leaves an ordinary request alone — ISR must not be disturbed", () => {
102
+ // The whole point: only drafted requests go dynamic. A shopper's request
103
+ // must reach the real, statically rendered route untouched.
104
+ const req = request("https://site.example/blog/hello");
105
+ expect(rewriteToDraftRoute(req, prepareDraft(req))).toBeNull();
106
+ });
107
+
108
+ it("never nests the prefix when middleware re-runs on the rewritten URL", () => {
109
+ const req = request("https://site.example/_draft/blog/hello?__draft=abc@v1");
110
+ expect(rewriteToDraftRoute(req, prepareDraft(req))).toBeNull();
111
+ });
112
+
113
+ it("does not rewrite when leaving draft mode", () => {
114
+ const req = request("https://site.example/blog/hello?__draft=off", "abc@v1");
115
+ expect(rewriteToDraftRoute(req, prepareDraft(req))).toBeNull();
116
+ });
117
+ });
@@ -40,6 +40,44 @@ export function prepareDraft(request: NextRequest): DraftMiddlewareDecision {
40
40
  return decideDraft(new URL(request.url), request.cookies.get(DRAFT_COOKIE)?.value ?? null);
41
41
  }
42
42
 
43
+ /**
44
+ * Route prefix a drafted request is rewritten onto.
45
+ *
46
+ * Deliberately a `_`-prefixed segment: Next treats those as private folders and
47
+ * excludes them from routing, so a site cannot accidentally expose it as a real
48
+ * page — it is reachable only through the middleware rewrite below.
49
+ */
50
+ export const DRAFT_ROUTE_PREFIX = "/_draft";
51
+
52
+ /**
53
+ * Rewrite a drafted request onto the dynamic draft route.
54
+ *
55
+ * This exists because `dynamic` / `revalidate` are STATIC route exports: a
56
+ * statically rendered page cannot become dynamic for one request. Making the
57
+ * real route dynamic to support drafts would cost every shopper their cached
58
+ * page, and — worse — a statically rendered draft would be cached and served to
59
+ * them. Rewriting sends only drafted requests to a route that is dynamic by
60
+ * construction, leaving ordinary traffic's ISR completely untouched.
61
+ *
62
+ * The original path is preserved in the rewritten pathname, so the draft route
63
+ * can resolve exactly the page the visitor asked for.
64
+ *
65
+ * Returns null when this request is not drafted, so callers fall through to
66
+ * their normal response.
67
+ */
68
+ export function rewriteToDraftRoute(
69
+ request: NextRequest,
70
+ decision: DraftMiddlewareDecision,
71
+ ): NextResponse | null {
72
+ if (!decision.pointer) return null;
73
+ const url = request.nextUrl.clone();
74
+ // Already rewritten (Next re-runs middleware on the rewritten URL in some
75
+ // configurations) — never nest the prefix.
76
+ if (url.pathname.startsWith(`${DRAFT_ROUTE_PREFIX}/`)) return null;
77
+ url.pathname = `${DRAFT_ROUTE_PREFIX}${url.pathname}`;
78
+ return NextResponse.rewrite(url);
79
+ }
80
+
43
81
  /**
44
82
  * Apply the cookie and the cache/indexing headers a draft response requires.
45
83
  *
package/src/index.ts CHANGED
@@ -19,6 +19,12 @@ export {
19
19
  registerDraftOverride,
20
20
  selectDraftPointer,
21
21
  } from "./draft";
22
+ export {
23
+ applyDraft,
24
+ DRAFT_ROUTE_PREFIX,
25
+ prepareDraft,
26
+ rewriteToDraftRoute,
27
+ } from "./draftMiddleware";
22
28
  export {
23
29
  decofileGET,
24
30
  decofilePOST,