@avocadostudio-ai/site-sdk 0.5.1 → 0.7.0
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/dist/cli/register-notice.d.ts +20 -0
- package/dist/cli/register-notice.js +34 -0
- package/dist/cli/register-notice.test.d.ts +1 -0
- package/dist/cli/register-notice.test.js +21 -0
- package/dist/cli/register.js +7 -2
- package/dist/draft.d.ts +1 -0
- package/dist/draft.js +3 -0
- package/dist/editor-render.d.ts +43 -0
- package/dist/editor-render.js +71 -0
- package/dist/editor-render.test.d.ts +1 -0
- package/dist/editor-render.test.js +44 -0
- package/dist/editor.d.ts +1 -1
- package/dist/editor.js +1 -1
- package/dist/lens/create-lens.d.ts +48 -0
- package/dist/lens/create-lens.js +349 -0
- package/dist/lens/index.d.ts +7 -0
- package/dist/lens/index.js +25 -0
- package/dist/lens/lens.test.d.ts +1 -0
- package/dist/lens/lens.test.js +221 -0
- package/dist/lens/register.d.ts +34 -0
- package/dist/lens/register.js +148 -0
- package/dist/lens/register.test.d.ts +1 -0
- package/dist/lens/register.test.js +81 -0
- package/dist/lens/sanity.d.ts +28 -0
- package/dist/lens/sanity.js +187 -0
- package/dist/lens/scalar-codecs.d.ts +26 -0
- package/dist/lens/scalar-codecs.js +92 -0
- package/dist/lens/storyblok.d.ts +28 -0
- package/dist/lens/storyblok.js +232 -0
- package/dist/lens/types.d.ts +243 -0
- package/dist/lens/types.js +28 -0
- package/dist/markers.d.ts +68 -0
- package/dist/markers.js +62 -0
- package/dist/markers.test.d.ts +1 -0
- package/dist/markers.test.js +35 -0
- package/dist/middleware.d.ts +1 -0
- package/dist/middleware.js +6 -0
- package/dist/proxy.d.ts +26 -0
- package/dist/proxy.js +88 -26
- package/dist/proxy.test.js +61 -2
- package/package.json +21 -5
package/dist/middleware.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { createEditorProxy } from "./proxy.js";
|
|
2
|
+
/*
|
|
3
|
+
* A Next 15 site reaches for this entry point, and a Next 15 site that already
|
|
4
|
+
* has a `middleware.ts` needs the two halves rather than the whole factory.
|
|
5
|
+
* Re-exported here so composing does not mean importing from two subpaths.
|
|
6
|
+
*/
|
|
7
|
+
export { trailingSlashRedirect, editorPreviewRewrite } from "./proxy.js";
|
|
2
8
|
/**
|
|
3
9
|
* Create a Next.js middleware function that rewrites editor/draft requests
|
|
4
10
|
* to a dynamic preview route, keeping the main page route fully static.
|
package/dist/proxy.d.ts
CHANGED
|
@@ -93,3 +93,29 @@ export declare function createEditorProxy(options?: EditorProxyOptions): {
|
|
|
93
93
|
matcher: string[];
|
|
94
94
|
};
|
|
95
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* Re-issue the trailing-slash redirect that `skipTrailingSlashRedirect` turned
|
|
98
|
+
* off, or `null` when the URL is already canonical.
|
|
99
|
+
*
|
|
100
|
+
* Exported because a site that already has a `middleware.ts` cannot use
|
|
101
|
+
* `createEditorProxy` as its whole middleware and has to compose — and the one
|
|
102
|
+
* line worth not writing twice is the first one below.
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* export function middleware(request: NextRequest) {
|
|
106
|
+
* const canonical = trailingSlashRedirect(request)
|
|
107
|
+
* if (canonical) return canonical
|
|
108
|
+
* return editorPreviewRewrite(request) ?? myOwnMiddleware(request)
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function trailingSlashRedirect(request: NextRequest): NextResponse | null;
|
|
113
|
+
/**
|
|
114
|
+
* Rewrite an editor request to the dynamic preview route, or `null` when the
|
|
115
|
+
* request is not one — which is the caller's cue to run its own middleware
|
|
116
|
+
* rather than a decision to pass the request through.
|
|
117
|
+
*
|
|
118
|
+
* The half of `createEditorProxy` a site with its own middleware needs; see
|
|
119
|
+
* {@link trailingSlashRedirect} for the composition.
|
|
120
|
+
*/
|
|
121
|
+
export declare function editorPreviewRewrite(request: NextRequest, options?: Pick<EditorProxyOptions, "previewRoute" | "editorParam" | "draftCookie">): NextResponse | null;
|
package/dist/proxy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { DEFAULT_PREVIEW_ROUTE, buildEditorMatcher } from "./editor-matcher.js";
|
|
3
|
+
import { EDITOR_RENDER_HEADER } from "./editor-render.js";
|
|
3
4
|
export { DEFAULT_PREVIEW_ROUTE, buildEditorMatcher } from "./editor-matcher.js";
|
|
4
5
|
/**
|
|
5
6
|
* Create a Next.js proxy function that rewrites editor/draft requests
|
|
@@ -33,8 +34,6 @@ export { DEFAULT_PREVIEW_ROUTE, buildEditorMatcher } from "./editor-matcher.js";
|
|
|
33
34
|
*/
|
|
34
35
|
export function createEditorProxy(options) {
|
|
35
36
|
const previewRoute = options?.previewRoute ?? DEFAULT_PREVIEW_ROUTE;
|
|
36
|
-
const editorParam = options?.editorParam ?? "__editor";
|
|
37
|
-
const draftCookie = options?.draftCookie === undefined ? "__prerender_bypass" : options.draftCookie;
|
|
38
37
|
const trailingSlash = options?.trailingSlash ?? false;
|
|
39
38
|
function proxy(request) {
|
|
40
39
|
/*
|
|
@@ -45,32 +44,95 @@ export function createEditorProxy(options) {
|
|
|
45
44
|
* publishes only at `/about/`.
|
|
46
45
|
*/
|
|
47
46
|
if (trailingSlash) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
* redirect built from a clone points at the URL it is trying to leave —
|
|
52
|
-
* a redirect loop, and one that only a browser would ever have shown us.
|
|
53
|
-
* A plain URL does no normalising. It also keeps `basePath`, which
|
|
54
|
-
* `nextUrl.pathname` has already stripped.
|
|
55
|
-
*/
|
|
56
|
-
const url = new URL(request.url);
|
|
57
|
-
if (url.pathname.length > 1 && !url.pathname.endsWith("/")) {
|
|
58
|
-
url.pathname = `${url.pathname}/`;
|
|
59
|
-
// 308, not 307: the method is preserved *and* the redirect is
|
|
60
|
-
// permanent, which is what Next's own trailing-slash redirect sends
|
|
61
|
-
// and what the site's existing search rankings were built on.
|
|
62
|
-
return NextResponse.redirect(url, 308);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const isEditor = request.nextUrl.searchParams.get(editorParam) === "1";
|
|
66
|
-
const hasDraftCookie = draftCookie !== false && request.cookies.has(draftCookie);
|
|
67
|
-
if (isEditor || hasDraftCookie) {
|
|
68
|
-
const url = request.nextUrl.clone();
|
|
69
|
-
url.pathname = `${previewRoute}${url.pathname}`;
|
|
70
|
-
return NextResponse.rewrite(url);
|
|
47
|
+
const redirect = trailingSlashRedirect(request);
|
|
48
|
+
if (redirect)
|
|
49
|
+
return redirect;
|
|
71
50
|
}
|
|
72
|
-
return NextResponse.next();
|
|
51
|
+
return editorPreviewRewrite(request, options) ?? NextResponse.next();
|
|
73
52
|
}
|
|
74
53
|
const config = { matcher: [buildEditorMatcher(previewRoute)] };
|
|
75
54
|
return { proxy, config };
|
|
76
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Re-issue the trailing-slash redirect that `skipTrailingSlashRedirect` turned
|
|
58
|
+
* off, or `null` when the URL is already canonical.
|
|
59
|
+
*
|
|
60
|
+
* Exported because a site that already has a `middleware.ts` cannot use
|
|
61
|
+
* `createEditorProxy` as its whole middleware and has to compose — and the one
|
|
62
|
+
* line worth not writing twice is the first one below.
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* export function middleware(request: NextRequest) {
|
|
66
|
+
* const canonical = trailingSlashRedirect(request)
|
|
67
|
+
* if (canonical) return canonical
|
|
68
|
+
* return editorPreviewRewrite(request) ?? myOwnMiddleware(request)
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function trailingSlashRedirect(request) {
|
|
73
|
+
/*
|
|
74
|
+
* Built from `request.url`, not from `request.nextUrl.clone()`. NextURL
|
|
75
|
+
* normalises a trailing slash back *off* when it stringifies, so a redirect
|
|
76
|
+
* built from a clone points at the URL it is trying to leave — every page on
|
|
77
|
+
* the site enters an infinite redirect, and only in a browser: `curl`
|
|
78
|
+
* without `-L` sees one perfectly ordinary 308. A plain URL does no
|
|
79
|
+
* normalising. It also keeps `basePath`, which `nextUrl.pathname` has
|
|
80
|
+
* already stripped.
|
|
81
|
+
*/
|
|
82
|
+
const url = new URL(request.url);
|
|
83
|
+
const { pathname } = url;
|
|
84
|
+
if (pathname.length <= 1 || pathname.endsWith("/"))
|
|
85
|
+
return null;
|
|
86
|
+
/*
|
|
87
|
+
* The set that must never gain a trailing slash. `createEditorProxy`'s own
|
|
88
|
+
* matcher already excludes all of it, so these are no-ops there — they are
|
|
89
|
+
* for the hand-composed case, where the site's matcher is its own and is
|
|
90
|
+
* usually wider. Redirecting `/api/editor/blocks` to `/api/editor/blocks/`
|
|
91
|
+
* here would re-create, by hand, the exact 308 that
|
|
92
|
+
* `skipTrailingSlashRedirect` was turned on to stop.
|
|
93
|
+
*/
|
|
94
|
+
if (pathname.startsWith("/api/") || pathname.startsWith("/_next/"))
|
|
95
|
+
return null;
|
|
96
|
+
if (pathname.slice(pathname.lastIndexOf("/") + 1).includes("."))
|
|
97
|
+
return null;
|
|
98
|
+
url.pathname = `${pathname}/`;
|
|
99
|
+
// 308, not 307: the method is preserved *and* the redirect is permanent,
|
|
100
|
+
// which is what Next's own trailing-slash redirect sends and what the site's
|
|
101
|
+
// existing search rankings were built on.
|
|
102
|
+
return NextResponse.redirect(url, 308);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Rewrite an editor request to the dynamic preview route, or `null` when the
|
|
106
|
+
* request is not one — which is the caller's cue to run its own middleware
|
|
107
|
+
* rather than a decision to pass the request through.
|
|
108
|
+
*
|
|
109
|
+
* The half of `createEditorProxy` a site with its own middleware needs; see
|
|
110
|
+
* {@link trailingSlashRedirect} for the composition.
|
|
111
|
+
*/
|
|
112
|
+
export function editorPreviewRewrite(request, options) {
|
|
113
|
+
const previewRoute = options?.previewRoute ?? DEFAULT_PREVIEW_ROUTE;
|
|
114
|
+
const editorParam = options?.editorParam ?? "__editor";
|
|
115
|
+
const draftCookie = options?.draftCookie === undefined ? "__prerender_bypass" : options.draftCookie;
|
|
116
|
+
const isEditor = request.nextUrl.searchParams.get(editorParam) === "1";
|
|
117
|
+
const hasDraftCookie = draftCookie !== false && request.cookies.has(draftCookie);
|
|
118
|
+
if (!isEditor && !hasDraftCookie)
|
|
119
|
+
return null;
|
|
120
|
+
const url = request.nextUrl.clone();
|
|
121
|
+
url.pathname = `${previewRoute}${url.pathname}`;
|
|
122
|
+
/*
|
|
123
|
+
* The one fact a layout can get at.
|
|
124
|
+
*
|
|
125
|
+
* A page learns it is being previewed from `resolveEditorContext(searchParams)`.
|
|
126
|
+
* A layout receives no `searchParams`, and the root layout is exactly where a
|
|
127
|
+
* site mounts its consent banner, analytics and tag manager — so every
|
|
128
|
+
* integration loads all three into the editor iframe until somebody notices
|
|
129
|
+
* the pageviews. Stamping the rewritten request lets `isEditorRender()` answer
|
|
130
|
+
* in a layout without the site threading anything down to it.
|
|
131
|
+
*
|
|
132
|
+
* Set, not appended: a client-supplied header of the same name is replaced, so
|
|
133
|
+
* what a layout reads here is the proxy's own answer.
|
|
134
|
+
*/
|
|
135
|
+
const headers = new Headers(request.headers);
|
|
136
|
+
headers.set(EDITOR_RENDER_HEADER, "1");
|
|
137
|
+
return NextResponse.rewrite(url, { request: { headers } });
|
|
138
|
+
}
|
package/dist/proxy.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { NextRequest } from "next/server";
|
|
4
|
-
import { buildEditorMatcher, createEditorProxy, DEFAULT_PREVIEW_ROUTE } from "./proxy.js";
|
|
3
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
4
|
+
import { buildEditorMatcher, createEditorProxy, editorPreviewRewrite, trailingSlashRedirect, DEFAULT_PREVIEW_ROUTE } from "./proxy.js";
|
|
5
5
|
import { createEditorMiddleware } from "./middleware.js";
|
|
6
6
|
/*
|
|
7
7
|
* The rewrite that keeps the published route static and sends editor traffic to
|
|
@@ -121,3 +121,62 @@ test("draftCookie: false leaves Next's draft cookie to whoever else is using it"
|
|
|
121
121
|
assert.equal(rewriteOf(proxy(request("https://site.test/about", "__prerender_bypass=abc"))), null, "a Sanity or Contentful preview must not be hijacked into Avocado's route");
|
|
122
122
|
assert.equal(rewriteOf(proxy(request("https://site.test/about?__editor=1"))), "https://site.test/preview-draft/about?__editor=1", "the explicit editor parameter still works — that is the whole point of the opt-out");
|
|
123
123
|
});
|
|
124
|
+
/*
|
|
125
|
+
* The two halves, on their own.
|
|
126
|
+
*
|
|
127
|
+
* A site that already has a `middleware.ts` cannot hand the whole file to
|
|
128
|
+
* `createEditorProxy` and has to compose — which until now meant re-deriving
|
|
129
|
+
* the trailing-slash redirect from the docs. One integration did, and wrote
|
|
130
|
+
* `request.nextUrl.clone()`, because that is what every Next example uses for
|
|
131
|
+
* a URL you intend to modify. A `NextURL` re-applies trailing-slash
|
|
132
|
+
* normalisation when it serialises, so the redirect stripped the slash it had
|
|
133
|
+
* just added and pointed at the URL it came from: every page on the site in an
|
|
134
|
+
* infinite redirect, reproducing only in a browser. `curl` without `-L` sees
|
|
135
|
+
* one perfectly ordinary 308, which is how it survived a session and a half.
|
|
136
|
+
*/
|
|
137
|
+
test("the redirect is built from request.url, not from a NextURL that would undo it", () => {
|
|
138
|
+
const response = trailingSlashRedirect(request("https://site.test/bern"));
|
|
139
|
+
assert.equal(response?.status, 308);
|
|
140
|
+
assert.equal(response?.headers.get("location"), "https://site.test/bern/", "a location without the slash is the loop: the browser asks again and gets the same answer");
|
|
141
|
+
});
|
|
142
|
+
test("an already-canonical URL, the root, and a file are left alone", () => {
|
|
143
|
+
assert.equal(trailingSlashRedirect(request("https://site.test/bern/")), null);
|
|
144
|
+
assert.equal(trailingSlashRedirect(request("https://site.test/")), null, "redirecting / never terminates");
|
|
145
|
+
assert.equal(trailingSlashRedirect(request("https://site.test/logo.svg")), null);
|
|
146
|
+
});
|
|
147
|
+
test("the API routes are never redirected, whatever the caller's matcher says", () => {
|
|
148
|
+
/*
|
|
149
|
+
* The proxy's own matcher excludes these, but a hand-composed middleware
|
|
150
|
+
* brings its own matcher and it is usually wider. A slash added here
|
|
151
|
+
* re-creates by hand the exact 308 on `/api/editor/*` that
|
|
152
|
+
* `skipTrailingSlashRedirect` was turned on to stop — and the symptom is a
|
|
153
|
+
* CORS error that names nothing.
|
|
154
|
+
*/
|
|
155
|
+
assert.equal(trailingSlashRedirect(request("https://site.test/api/editor/blocks")), null);
|
|
156
|
+
assert.equal(trailingSlashRedirect(request("https://site.test/_next/static/chunk")), null);
|
|
157
|
+
});
|
|
158
|
+
test("the redirect keeps the query string when called directly too", () => {
|
|
159
|
+
const response = trailingSlashRedirect(request("https://site.test/bern?__editor=1"));
|
|
160
|
+
assert.equal(response?.headers.get("location"), "https://site.test/bern/?__editor=1");
|
|
161
|
+
});
|
|
162
|
+
test("editorPreviewRewrite answers null for a visitor, so the caller runs its own middleware", () => {
|
|
163
|
+
assert.equal(editorPreviewRewrite(request("https://site.test/about")), null, "null is 'not mine', not 'pass this through' — a composing caller has its own work to do");
|
|
164
|
+
assert.equal(rewriteOf(editorPreviewRewrite(request("https://site.test/about?__editor=1"))), "https://site.test/preview-draft/about?__editor=1");
|
|
165
|
+
});
|
|
166
|
+
test("the composed form behaves like the factory it was extracted from", () => {
|
|
167
|
+
const { proxy } = createEditorProxy({ trailingSlash: true });
|
|
168
|
+
const compose = (r) => trailingSlashRedirect(r) ?? editorPreviewRewrite(r) ?? NextResponse.next();
|
|
169
|
+
for (const url of [
|
|
170
|
+
"https://site.test/bern",
|
|
171
|
+
"https://site.test/bern/",
|
|
172
|
+
"https://site.test/bern?__editor=1",
|
|
173
|
+
"https://site.test/bern/?__editor=1",
|
|
174
|
+
"https://site.test/"
|
|
175
|
+
]) {
|
|
176
|
+
const fromFactory = proxy(request(url));
|
|
177
|
+
const fromParts = compose(request(url));
|
|
178
|
+
assert.equal(fromParts.status, fromFactory.status, url);
|
|
179
|
+
assert.equal(fromParts.headers.get("location"), fromFactory.headers.get("location"), url);
|
|
180
|
+
assert.equal(rewriteOf(fromParts), rewriteOf(fromFactory), url);
|
|
181
|
+
}
|
|
182
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/site-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,6 +26,21 @@
|
|
|
26
26
|
"import": "./dist/markers.js",
|
|
27
27
|
"default": "./dist/markers.js"
|
|
28
28
|
},
|
|
29
|
+
"./lens": {
|
|
30
|
+
"types": "./dist/lens/index.d.ts",
|
|
31
|
+
"import": "./dist/lens/index.js",
|
|
32
|
+
"default": "./dist/lens/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./lens/storyblok": {
|
|
35
|
+
"types": "./dist/lens/storyblok.d.ts",
|
|
36
|
+
"import": "./dist/lens/storyblok.js",
|
|
37
|
+
"default": "./dist/lens/storyblok.js"
|
|
38
|
+
},
|
|
39
|
+
"./lens/sanity": {
|
|
40
|
+
"types": "./dist/lens/sanity.d.ts",
|
|
41
|
+
"import": "./dist/lens/sanity.js",
|
|
42
|
+
"default": "./dist/lens/sanity.js"
|
|
43
|
+
},
|
|
29
44
|
"./blocks": {
|
|
30
45
|
"types": "./dist/blocks.d.ts",
|
|
31
46
|
"import": "./dist/blocks.js",
|
|
@@ -122,16 +137,17 @@
|
|
|
122
137
|
],
|
|
123
138
|
"dependencies": {
|
|
124
139
|
"zod": "^4.3.6",
|
|
125
|
-
"@avocadostudio-ai/blocks": "^0.
|
|
126
|
-
"@avocadostudio-ai/
|
|
127
|
-
"@avocadostudio-ai/shared": "^0.
|
|
140
|
+
"@avocadostudio-ai/blocks": "^0.7.0",
|
|
141
|
+
"@avocadostudio-ai/richtext": "^0.7.0",
|
|
142
|
+
"@avocadostudio-ai/shared": "^0.7.0",
|
|
143
|
+
"@avocadostudio-ai/preview-adapter": "^0.7.0"
|
|
128
144
|
},
|
|
129
145
|
"peerDependencies": {
|
|
130
146
|
"next": ">=15.0.0",
|
|
131
147
|
"react": ">=19.0.0",
|
|
132
148
|
"react-dom": ">=19.0.0",
|
|
133
149
|
"better-sqlite3": ">=12.0.0",
|
|
134
|
-
"@avocadostudio-ai/orchestrator-core": "^0.
|
|
150
|
+
"@avocadostudio-ai/orchestrator-core": "^0.7.0"
|
|
135
151
|
},
|
|
136
152
|
"peerDependenciesMeta": {
|
|
137
153
|
"@avocadostudio-ai/orchestrator-core": {
|