@avocadostudio-ai/site-sdk 0.7.0 → 0.9.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/README.md +1 -1
- package/dist/create-site-page.js +47 -84
- package/dist/draft-common.d.ts +1 -0
- package/dist/draft-common.js +13 -0
- package/dist/draft-context-core.d.ts +1 -1
- package/dist/draft-context-core.js +2 -3
- package/dist/draft-context.d.ts +1 -1
- package/dist/draft-context.js +1 -3
- package/dist/draft-routes.d.ts +7 -0
- package/dist/draft-routes.js +8 -3
- package/dist/editor-api-handler-core.d.ts +85 -0
- package/dist/editor-api-handler-core.js +101 -0
- package/dist/editor-api-handler.d.ts +7 -49
- package/dist/editor-api-handler.js +12 -66
- package/dist/editor-rewrite-core.d.ts +71 -0
- package/dist/editor-rewrite-core.js +37 -0
- package/dist/lens/register.d.ts +6 -8
- package/dist/lens/register.js +203 -29
- package/dist/lens/types.d.ts +36 -0
- package/dist/page-metadata.d.ts +21 -0
- package/dist/page-metadata.js +64 -0
- package/dist/proxy.d.ts +9 -30
- package/dist/proxy.js +16 -22
- package/dist/resolve-page-render.d.ts +94 -0
- package/dist/resolve-page-render.js +80 -0
- package/dist/routes-core.d.ts +12 -0
- package/dist/routes-core.js +24 -0
- package/package.json +25 -15
- package/dist/cli/register-notice.test.d.ts +0 -1
- package/dist/cli/register-notice.test.js +0 -21
- package/dist/draft-context-core.test.d.ts +0 -10
- package/dist/draft-context-core.test.js +0 -146
- package/dist/draft-fetch.test.d.ts +0 -1
- package/dist/draft-fetch.test.js +0 -153
- package/dist/editor-api-blocks-catalogue.test.d.ts +0 -1
- package/dist/editor-api-blocks-catalogue.test.js +0 -73
- package/dist/editor-cors.test.d.ts +0 -1
- package/dist/editor-cors.test.js +0 -85
- package/dist/editor-render.test.d.ts +0 -1
- package/dist/editor-render.test.js +0 -44
- package/dist/lens/lens.test.d.ts +0 -1
- package/dist/lens/lens.test.js +0 -221
- package/dist/lens/register.test.d.ts +0 -1
- package/dist/lens/register.test.js +0 -81
- package/dist/manifest-utils.test.d.ts +0 -1
- package/dist/manifest-utils.test.js +0 -72
- package/dist/markers.test.d.ts +0 -1
- package/dist/markers.test.js +0 -35
- package/dist/next-config.test.d.ts +0 -1
- package/dist/next-config.test.js +0 -393
- package/dist/page-metadata.test.d.ts +0 -1
- package/dist/page-metadata.test.js +0 -105
- package/dist/proxy.test.d.ts +0 -1
- package/dist/proxy.test.js +0 -182
- package/dist/publish/field-diff.test.d.ts +0 -1
- package/dist/publish/field-diff.test.js +0 -350
package/README.md
CHANGED
|
@@ -604,7 +604,7 @@ createOrchestrator({
|
|
|
604
604
|
`draftPath` is either a prefix the page slug is appended to (`/avocado` →
|
|
605
605
|
`/avocado/de/events`) or a template naming where the slug goes
|
|
606
606
|
(`/preview/{slug}/draft`). It defaults to `/preview-draft`, which is what
|
|
607
|
-
`create-
|
|
607
|
+
`create-avocado-site` scaffolds — **if you wired Avocado into a site you
|
|
608
608
|
already had, set this**, or every draft screenshot comes back a cheerful 200 and
|
|
609
609
|
a picture of your 404 page.
|
|
610
610
|
|
package/dist/create-site-page.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
2
|
-
import { draftMode } from "next/headers";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { draftMode, cookies } from "next/headers";
|
|
3
3
|
import { connection } from "next/server";
|
|
4
4
|
import { notFound } from "next/navigation";
|
|
5
5
|
import { SharedBlockRenderer, BlocksHydrator } from "@avocadostudio-ai/blocks";
|
|
6
6
|
import { buildSlug } from "./index.js";
|
|
7
|
-
import { resolveEditorContext
|
|
7
|
+
import { resolveEditorContext } from "./draft.js";
|
|
8
8
|
import { renderBlocks, EditorOverlay } from "./editor.js";
|
|
9
|
-
import {
|
|
9
|
+
import { resolvePageRender } from "./resolve-page-render.js";
|
|
10
10
|
import { buildPageMetadata } from "./page-metadata.js";
|
|
11
11
|
function resolve(config) {
|
|
12
12
|
return {
|
|
@@ -30,33 +30,30 @@ function makeGenerateStaticParams(cmsGetSlugs) {
|
|
|
30
30
|
}));
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
]);
|
|
33
|
+
function nextDraftAdapter(draft, getCookie) {
|
|
34
|
+
return { isDraftMode: draft.isEnabled, getCookie };
|
|
35
|
+
}
|
|
36
|
+
/** The site chrome around whatever the outcome turned out to be. */
|
|
37
|
+
function withChrome(chrome, c, body, after) {
|
|
38
|
+
return (_jsxs(_Fragment, { children: [c.chrome && _jsx(SharedBlockRenderer, { block: chrome.header }), body, c.chrome && c.footer ? _jsx(SharedBlockRenderer, { block: c.footer }) : null, after] }));
|
|
39
|
+
}
|
|
40
|
+
function renderOutcome(outcome, c) {
|
|
39
41
|
/*
|
|
40
42
|
* `notFound()`, not a rendered 404 body. The previous version returned an
|
|
41
43
|
* <h1>404</h1> with HTTP 200, which is a soft 404: crawlers index it as a
|
|
42
44
|
* real page, and the host app's own `not-found.tsx` never runs. Consumers who
|
|
43
45
|
* want the site chrome around their 404 get it by adding `app/not-found.tsx`,
|
|
44
46
|
* which is where Next puts that and where a layout can wrap it.
|
|
45
|
-
*
|
|
46
|
-
* Before building the nav, since none of it survives the throw.
|
|
47
47
|
*/
|
|
48
|
-
if (
|
|
48
|
+
if (outcome.kind === "not-found")
|
|
49
49
|
notFound();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
58
|
-
const chromeHeader = buildSiteHeaderBlock({ navItems, siteName, siteLogo, activePath: slug });
|
|
59
|
-
return (_jsxs(_Fragment, { children: [c.chrome && _jsx(SharedBlockRenderer, { block: chromeHeader }), _jsxs("main", { children: [renderBlocks(page.blocks), _jsx(BlocksHydrator, {})] }), c.chrome && c.footer ? _jsx(SharedBlockRenderer, { block: c.footer }) : null] }));
|
|
50
|
+
if (outcome.kind === "draft-unavailable") {
|
|
51
|
+
return withChrome(outcome.chrome, c, _jsxs("main", { style: { padding: "4rem", textAlign: "center" }, children: [_jsx("h1", { children: "Draft unavailable" }), _jsxs("p", { children: ["Could not load draft content for ", outcome.slug, "."] })] }));
|
|
52
|
+
}
|
|
53
|
+
if (!outcome.editor) {
|
|
54
|
+
return withChrome(outcome.chrome, c, _jsxs("main", { children: [renderBlocks(outcome.page.blocks), _jsx(BlocksHydrator, {})] }));
|
|
55
|
+
}
|
|
56
|
+
return withChrome(outcome.chrome, c, _jsx("main", { className: "editor-mode", children: renderBlocks(outcome.page.blocks, { editable: true }) }), _jsx(EditorOverlay, { slug: outcome.slug, editorOrigin: outcome.editor.editorOrigin }));
|
|
60
57
|
}
|
|
61
58
|
/**
|
|
62
59
|
* Metadata for a page, mirroring the render's own choice of source.
|
|
@@ -90,57 +87,33 @@ function makeGenerateMetadata(c, mode) {
|
|
|
90
87
|
return decorate(buildPageMetadata(page, { siteName: c.siteName }), page);
|
|
91
88
|
};
|
|
92
89
|
}
|
|
93
|
-
async function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
defaultSession: c.defaultSession,
|
|
103
|
-
defaultSiteId: c.siteId,
|
|
104
|
-
});
|
|
105
|
-
const session = editorCtx?.session ?? c.defaultSession;
|
|
106
|
-
const currentSiteId = editorCtx?.siteId ?? c.siteId;
|
|
107
|
-
const [page, navSlugs, siteConfig] = await Promise.all([
|
|
108
|
-
fetchEditorPage(slug, session, currentSiteId).then((p) => p ?? c.cmsGetPage(slug)),
|
|
109
|
-
fetchEditorSlugs(session, currentSiteId).then((s) => (s.length > 0 ? s : c.cmsGetSlugs())),
|
|
110
|
-
c.cmsGetSiteConfig ? c.cmsGetSiteConfig() : Promise.resolve({}),
|
|
111
|
-
]);
|
|
112
|
-
const editorOrigin = editorCtx?.editorOrigin ?? "";
|
|
113
|
-
const editorQuery = (() => {
|
|
114
|
-
const p = new URLSearchParams({ session, siteId: currentSiteId });
|
|
115
|
-
if (editorOrigin)
|
|
116
|
-
p.set("editorOrigin", editorOrigin);
|
|
117
|
-
return `?${p.toString()}`;
|
|
118
|
-
})();
|
|
119
|
-
const { navItems, siteName, siteLogo } = buildNavItems({
|
|
120
|
-
navSlugs,
|
|
121
|
-
currentSlug: slug,
|
|
122
|
-
siteConfig,
|
|
123
|
-
siteId: c.siteId,
|
|
124
|
-
editorQuery,
|
|
125
|
-
defaultLogo: c.defaultLogo,
|
|
126
|
-
});
|
|
127
|
-
const chromeHeader = buildSiteHeaderBlock({ navItems, siteName, siteLogo, activePath: slug });
|
|
128
|
-
if (!page) {
|
|
129
|
-
return (_jsxs(_Fragment, { children: [c.chrome && _jsx(SharedBlockRenderer, { block: chromeHeader }), _jsxs("main", { style: { padding: "4rem", textAlign: "center" }, children: [_jsx("h1", { children: "Draft unavailable" }), _jsxs("p", { children: ["Could not load draft content for ", slug, "."] })] }), c.chrome && c.footer ? _jsx(SharedBlockRenderer, { block: c.footer }) : null] }));
|
|
90
|
+
async function render(slug, search, c, mode) {
|
|
91
|
+
if (mode !== "static") {
|
|
92
|
+
/*
|
|
93
|
+
* `connection()`, not `unstable_noStore()`. Same effect — this render
|
|
94
|
+
* depends on a live request and must never be prerendered or cached — but
|
|
95
|
+
* it is the stable primitive Next ships for saying so, and the SDK had
|
|
96
|
+
* exactly one `unstable_*` import left in it.
|
|
97
|
+
*/
|
|
98
|
+
await connection();
|
|
130
99
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
100
|
+
const jar = mode === "static" ? null : await cookies();
|
|
101
|
+
const draft = mode === "static" ? null : await draftMode();
|
|
102
|
+
const outcome = await resolvePageRender({
|
|
103
|
+
slug,
|
|
104
|
+
mode,
|
|
105
|
+
search,
|
|
106
|
+
config: {
|
|
107
|
+
siteId: c.siteId,
|
|
108
|
+
session: c.defaultSession,
|
|
109
|
+
getPage: c.cmsGetPage,
|
|
110
|
+
getSlugs: c.cmsGetSlugs,
|
|
111
|
+
getSiteConfig: c.cmsGetSiteConfig,
|
|
112
|
+
defaultLogo: c.defaultLogo,
|
|
113
|
+
},
|
|
114
|
+
adapter: draft && jar ? nextDraftAdapter(draft, (name) => jar.get(name)?.value) : undefined,
|
|
137
115
|
});
|
|
138
|
-
|
|
139
|
-
const editorMode = draft.isEnabled || !!editorCtx;
|
|
140
|
-
if (editorMode) {
|
|
141
|
-
return renderPreview(slug, search, c);
|
|
142
|
-
}
|
|
143
|
-
return renderStatic(slug, c);
|
|
116
|
+
return renderOutcome(outcome, c);
|
|
144
117
|
}
|
|
145
118
|
/**
|
|
146
119
|
* Create a Next.js page component with full editor integration.
|
|
@@ -156,23 +129,13 @@ export function createSitePage(config) {
|
|
|
156
129
|
if (mode === "static") {
|
|
157
130
|
async function Page({ params }) {
|
|
158
131
|
const { slug: slugParts } = await params;
|
|
159
|
-
|
|
160
|
-
return renderStatic(slug, c);
|
|
161
|
-
}
|
|
162
|
-
return { Page, generateStaticParams, generateMetadata };
|
|
163
|
-
}
|
|
164
|
-
if (mode === "preview") {
|
|
165
|
-
async function Page({ params, searchParams }) {
|
|
166
|
-
const [{ slug: slugParts }, search] = await Promise.all([params, searchParams]);
|
|
167
|
-
const slug = buildSlug(slugParts);
|
|
168
|
-
return renderPreview(slug, search, c);
|
|
132
|
+
return render(buildSlug(slugParts), undefined, c, "static");
|
|
169
133
|
}
|
|
170
134
|
return { Page, generateStaticParams, generateMetadata };
|
|
171
135
|
}
|
|
172
136
|
async function Page({ params, searchParams }) {
|
|
173
137
|
const [{ slug: slugParts }, search] = await Promise.all([params, searchParams]);
|
|
174
|
-
|
|
175
|
-
return renderAuto(slug, search, c);
|
|
138
|
+
return render(buildSlug(slugParts), search, c, mode);
|
|
176
139
|
}
|
|
177
140
|
return { Page, generateStaticParams, generateMetadata };
|
|
178
141
|
}
|
package/dist/draft-common.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const DRAFT_SESSION_COOKIE = "editor_draft_session";
|
|
2
2
|
export declare const DRAFT_SITE_COOKIE = "editor_draft_site_id";
|
|
3
3
|
export declare const EDITOR_ORIGIN_COOKIE = "editor_origin";
|
|
4
|
+
export declare function single(value: string | string[] | undefined): string | undefined;
|
|
4
5
|
export declare function normalizeOrigin(value: string | null | undefined): string | undefined;
|
|
5
6
|
/**
|
|
6
7
|
* Editor origins this site will talk to.
|
package/dist/draft-common.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
export const DRAFT_SESSION_COOKIE = "editor_draft_session";
|
|
2
2
|
export const DRAFT_SITE_COOKIE = "editor_draft_site_id";
|
|
3
3
|
export const EDITOR_ORIGIN_COOKIE = "editor_origin";
|
|
4
|
+
/*
|
|
5
|
+
* A query value that arrived more than once is not a value. Next hands
|
|
6
|
+
* repeated params over as an array, and every caller here wants the single
|
|
7
|
+
* string or nothing — `?secret=a&secret=b` is not two secrets to try, it is a
|
|
8
|
+
* malformed request.
|
|
9
|
+
*
|
|
10
|
+
* It lives in this leaf module rather than beside `resolveEditorContext`
|
|
11
|
+
* because the framework-agnostic core needs it too, and importing it from the
|
|
12
|
+
* Next module pulled `next/headers` into every runtime that touched the core.
|
|
13
|
+
*/
|
|
14
|
+
export function single(value) {
|
|
15
|
+
return typeof value === "string" ? value : undefined;
|
|
16
|
+
}
|
|
4
17
|
export function normalizeOrigin(value) {
|
|
5
18
|
const raw = value?.trim();
|
|
6
19
|
if (!raw)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DraftContext, SearchParamsRecord } from "./types.ts";
|
|
2
|
-
export { single } from "./draft-
|
|
2
|
+
export { single } from "./draft-common.ts";
|
|
3
3
|
export type DraftModeAdapter = {
|
|
4
4
|
isDraftMode: boolean;
|
|
5
5
|
getCookie: (name: string) => string | undefined;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { validateDraftSecret } from "@avocadostudio-ai/shared";
|
|
2
|
-
import { DRAFT_SESSION_COOKIE, DRAFT_SITE_COOKIE, EDITOR_ORIGIN_COOKIE, normalizeOrigin, resolveTrustedEditorOrigin } from "./draft-common.js";
|
|
3
|
-
|
|
4
|
-
export { single } from "./draft-context.js";
|
|
2
|
+
import { DRAFT_SESSION_COOKIE, DRAFT_SITE_COOKIE, EDITOR_ORIGIN_COOKIE, normalizeOrigin, resolveTrustedEditorOrigin, single } from "./draft-common.js";
|
|
3
|
+
export { single } from "./draft-common.js";
|
|
5
4
|
export async function resolveDraftContextCore(searchParams, adapter, options) {
|
|
6
5
|
const isDev = process.env.NODE_ENV !== "production";
|
|
7
6
|
/*
|
package/dist/draft-context.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SearchParamsRecord } from "./types.ts";
|
|
2
2
|
import type { DraftContext } from "./types.ts";
|
|
3
|
-
export
|
|
3
|
+
export { single } from "./draft-common.ts";
|
|
4
4
|
export declare function resolveEditorContext(searchParams: SearchParamsRecord, options?: {
|
|
5
5
|
defaultSession?: string;
|
|
6
6
|
defaultSiteId?: string;
|
package/dist/draft-context.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { draftMode, cookies } from "next/headers";
|
|
2
2
|
import { resolveDraftContextCore } from "./draft-context-core.js";
|
|
3
|
-
export
|
|
4
|
-
return typeof value === "string" ? value : undefined;
|
|
5
|
-
}
|
|
3
|
+
export { single } from "./draft-common.js";
|
|
6
4
|
export async function resolveEditorContext(searchParams, options) {
|
|
7
5
|
const jar = await cookies();
|
|
8
6
|
const draft = await draftMode();
|
package/dist/draft-routes.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
import type { DraftRouteAdapter } from "./draft-routes-core.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Next's answers to the three things the draft routes need from a framework.
|
|
4
|
+
* Exported because the editor API handler builds its own core and has to hand
|
|
5
|
+
* the same adapter down.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createNextDraftAdapter(): DraftRouteAdapter;
|
|
1
8
|
export declare function createDraftEnableHandler(): (request: Request) => Promise<Response>;
|
|
2
9
|
export declare function createDraftDisableHandler(): (request: Request) => Promise<Response>;
|
package/dist/draft-routes.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { draftMode } from "next/headers";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
3
|
import { createDraftEnableHandlerCore, createDraftDisableHandlerCore } from "./draft-routes-core.js";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Next's answers to the three things the draft routes need from a framework.
|
|
6
|
+
* Exported because the editor API handler builds its own core and has to hand
|
|
7
|
+
* the same adapter down.
|
|
8
|
+
*/
|
|
9
|
+
export function createNextDraftAdapter() {
|
|
5
10
|
return {
|
|
6
11
|
enableDraftMode: async () => { (await draftMode()).enable(); },
|
|
7
12
|
disableDraftMode: async () => { (await draftMode()).disable(); },
|
|
@@ -20,8 +25,8 @@ function createNextAdapter() {
|
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
27
|
export function createDraftEnableHandler() {
|
|
23
|
-
return createDraftEnableHandlerCore(
|
|
28
|
+
return createDraftEnableHandlerCore(createNextDraftAdapter());
|
|
24
29
|
}
|
|
25
30
|
export function createDraftDisableHandler() {
|
|
26
|
-
return createDraftDisableHandlerCore(
|
|
31
|
+
return createDraftDisableHandlerCore(createNextDraftAdapter());
|
|
27
32
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DraftRouteAdapter } from "./draft-routes-core.ts";
|
|
2
|
+
import type { OnPublishFn } from "./editor-routes.ts";
|
|
3
|
+
import { type BlockManifest } from "./editor-manifest.ts";
|
|
4
|
+
import type { PageDoc } from "./types.ts";
|
|
5
|
+
import { type SiteConfig } from "@avocadostudio-ai/shared";
|
|
6
|
+
export interface EditorApiHandlerConfig {
|
|
7
|
+
getPages: () => PageDoc[] | Promise<PageDoc[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Optional site-config getter. When provided, `/api/editor/pages` returns
|
|
10
|
+
* `{ pages, siteConfig }` so the orchestrator's publish-diff can show
|
|
11
|
+
* header-chrome changes (name/logo/navLabels/navGroups) alongside page diffs.
|
|
12
|
+
*/
|
|
13
|
+
getSiteConfig?: () => SiteConfig | undefined | Promise<SiteConfig | undefined>;
|
|
14
|
+
getManifest?: () => BlockManifest;
|
|
15
|
+
/**
|
|
16
|
+
* Register the site's block schemas before each manifest build. Use this
|
|
17
|
+
* instead of relying on side-effect import order — Next.js's dev bundler
|
|
18
|
+
* does not reliably honor source-order side effects across RSC / SSR / API
|
|
19
|
+
* route layers, so a canonical re-registration from `@avocadostudio-ai/shared`
|
|
20
|
+
* can silently clobber the site's overrides. Called inside `getManifest`,
|
|
21
|
+
* so registration is guaranteed to be last at request time. `registerBlock`
|
|
22
|
+
* is idempotent, so repeated calls are cheap.
|
|
23
|
+
*
|
|
24
|
+
* Composes with `getManifest`: when both are set, `registerBlocks` runs
|
|
25
|
+
* first, then `getManifest` is invoked.
|
|
26
|
+
*/
|
|
27
|
+
registerBlocks?: () => void;
|
|
28
|
+
/**
|
|
29
|
+
* The block types this site renders, narrowing `/api/editor/blocks` to them.
|
|
30
|
+
*
|
|
31
|
+
* `createOrchestrator` takes the same option and declares the same catalogue,
|
|
32
|
+
* and for a while that was assumed to be enough for both routes: the
|
|
33
|
+
* declaration lives on `globalThis`, so one handler's call is visible to the
|
|
34
|
+
* other. What that reasoning missed is *when* it runs. A Next route module is
|
|
35
|
+
* evaluated on the first request to that route, so a site whose editor asks
|
|
36
|
+
* for `/api/editor/blocks` before anything has touched `/api/avocado/*` gets
|
|
37
|
+
* the manifest built before `createOrchestrator` has ever been called — and
|
|
38
|
+
* the answer is the site's own blocks plus all of Avocado's built-ins, which
|
|
39
|
+
* the site has no renderer for.
|
|
40
|
+
*
|
|
41
|
+
* Declaring it here closes that window, and passing it to both handlers is
|
|
42
|
+
* harmless: the declaration records names and the manifest is rebuilt per
|
|
43
|
+
* request.
|
|
44
|
+
*/
|
|
45
|
+
blockTypes?: readonly string[];
|
|
46
|
+
onPublish?: OnPublishFn;
|
|
47
|
+
/** Secret token required for publish requests. Checked against x-publish-token header. */
|
|
48
|
+
publishSecret?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface EditorApiCoreConfig extends EditorApiHandlerConfig {
|
|
51
|
+
/**
|
|
52
|
+
* How this runtime enables draft mode and builds a redirect. Next has
|
|
53
|
+
* `draftMode()` and `NextResponse.redirect`; every other host has to say so
|
|
54
|
+
* itself, which is the whole of what the draft routes need from a framework.
|
|
55
|
+
*/
|
|
56
|
+
draftAdapter: DraftRouteAdapter;
|
|
57
|
+
/**
|
|
58
|
+
* Where the catch-all is mounted, used to derive the route from the URL when
|
|
59
|
+
* the host does not hand over path segments of its own. Next's catch-all
|
|
60
|
+
* supplies them; Astro's `[...path]` also does, but a plain `fetch` handler
|
|
61
|
+
* mounted by hand has only the URL.
|
|
62
|
+
*/
|
|
63
|
+
basePath?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The editor API, with nothing framework-shaped left in it.
|
|
67
|
+
*
|
|
68
|
+
* Each handler takes the request and, optionally, the path segments beneath the
|
|
69
|
+
* mount point. A host that has them (any catch-all route) passes them; one that
|
|
70
|
+
* does not gets them derived from the URL against `basePath`. Everything below
|
|
71
|
+
* this line — matching, CORS, the manifest, publish — was already portable; it
|
|
72
|
+
* was reachable only through a signature naming a Next route context.
|
|
73
|
+
*
|
|
74
|
+
* Routes:
|
|
75
|
+
* - `/api/editor/draft` → enable draft mode
|
|
76
|
+
* - `/api/editor/draft/disable` → disable draft mode
|
|
77
|
+
* - `/api/editor/blocks` → block manifest
|
|
78
|
+
* - `/api/editor/pages` → published pages
|
|
79
|
+
* - `/api/editor/publish` → publish content (POST)
|
|
80
|
+
*/
|
|
81
|
+
export declare function createEditorApiHandlerCore(config: EditorApiCoreConfig): {
|
|
82
|
+
GET: (request: Request, path?: string[]) => Promise<Response>;
|
|
83
|
+
POST: (request: Request, path?: string[]) => Promise<Response>;
|
|
84
|
+
OPTIONS: (request: Request) => Response;
|
|
85
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createDraftEnableHandlerCore, createDraftDisableHandlerCore } from "./draft-routes-core.js";
|
|
2
|
+
import { createBlocksHandler, createPagesHandler, createPublishHandler } from "./editor-routes.js";
|
|
3
|
+
import { applyEditorCors } from "./editor-cors.js";
|
|
4
|
+
import { checkIntegrationOnce } from "./integration-check.js";
|
|
5
|
+
import { buildBlockManifest } from "./editor-manifest.js";
|
|
6
|
+
import { declareBlockCatalogue } from "@avocadostudio-ai/shared";
|
|
7
|
+
/**
|
|
8
|
+
* The editor API, with nothing framework-shaped left in it.
|
|
9
|
+
*
|
|
10
|
+
* Each handler takes the request and, optionally, the path segments beneath the
|
|
11
|
+
* mount point. A host that has them (any catch-all route) passes them; one that
|
|
12
|
+
* does not gets them derived from the URL against `basePath`. Everything below
|
|
13
|
+
* this line — matching, CORS, the manifest, publish — was already portable; it
|
|
14
|
+
* was reachable only through a signature naming a Next route context.
|
|
15
|
+
*
|
|
16
|
+
* Routes:
|
|
17
|
+
* - `/api/editor/draft` → enable draft mode
|
|
18
|
+
* - `/api/editor/draft/disable` → disable draft mode
|
|
19
|
+
* - `/api/editor/blocks` → block manifest
|
|
20
|
+
* - `/api/editor/pages` → published pages
|
|
21
|
+
* - `/api/editor/publish` → publish content (POST)
|
|
22
|
+
*/
|
|
23
|
+
export function createEditorApiHandlerCore(config) {
|
|
24
|
+
const draftEnable = createDraftEnableHandlerCore(config.draftAdapter);
|
|
25
|
+
const draftDisable = createDraftDisableHandlerCore(config.draftAdapter);
|
|
26
|
+
const basePath = (config.basePath ?? "/api/editor").replace(/\/+$/, "");
|
|
27
|
+
const needsPreamble = Boolean(config.registerBlocks || config.blockTypes);
|
|
28
|
+
const manifestBuilder = needsPreamble
|
|
29
|
+
? () => {
|
|
30
|
+
config.registerBlocks?.();
|
|
31
|
+
if (config.blockTypes)
|
|
32
|
+
declareBlockCatalogue(config.blockTypes);
|
|
33
|
+
return (config.getManifest ?? buildBlockManifest)();
|
|
34
|
+
}
|
|
35
|
+
: config.getManifest;
|
|
36
|
+
const blocksHandler = createBlocksHandler(manifestBuilder ? { getManifest: manifestBuilder } : undefined);
|
|
37
|
+
const pagesHandler = createPagesHandler(config.getPages, config.getSiteConfig);
|
|
38
|
+
const publishHandler = config.onPublish
|
|
39
|
+
? createPublishHandler(config.onPublish, { publishSecret: config.publishSecret })
|
|
40
|
+
: null;
|
|
41
|
+
/*
|
|
42
|
+
* `/api/editor/draft/disable` is two segments, so the fallback cannot simply
|
|
43
|
+
* take the last one. Trailing slashes and an exactly-`basePath` request both
|
|
44
|
+
* have to come out as no segments rather than as one empty one.
|
|
45
|
+
*/
|
|
46
|
+
function pathFromUrl(request) {
|
|
47
|
+
const { pathname } = new URL(request.url);
|
|
48
|
+
const rest = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
49
|
+
return rest.split("/").filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
function matchRoute(path) {
|
|
52
|
+
const key = path.join("/");
|
|
53
|
+
if (key === "draft")
|
|
54
|
+
return "draft-enable";
|
|
55
|
+
if (key === "draft/disable")
|
|
56
|
+
return "draft-disable";
|
|
57
|
+
if (key === "blocks")
|
|
58
|
+
return "blocks";
|
|
59
|
+
if (key === "pages")
|
|
60
|
+
return "pages";
|
|
61
|
+
if (key === "publish")
|
|
62
|
+
return "publish";
|
|
63
|
+
return "not-found";
|
|
64
|
+
}
|
|
65
|
+
function notFound(request) {
|
|
66
|
+
return applyEditorCors(new Response(JSON.stringify({ error: "Not found" }), {
|
|
67
|
+
status: 404,
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
}), request.headers.get("origin"));
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
async GET(request, path) {
|
|
73
|
+
checkIntegrationOnce();
|
|
74
|
+
const route = matchRoute(path ?? pathFromUrl(request));
|
|
75
|
+
switch (route) {
|
|
76
|
+
case "draft-enable":
|
|
77
|
+
return draftEnable(request);
|
|
78
|
+
case "draft-disable":
|
|
79
|
+
return draftDisable(request);
|
|
80
|
+
case "blocks":
|
|
81
|
+
return blocksHandler.GET(request);
|
|
82
|
+
case "pages":
|
|
83
|
+
return pagesHandler.GET(request);
|
|
84
|
+
default:
|
|
85
|
+
return notFound(request);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async POST(request, path) {
|
|
89
|
+
const route = matchRoute(path ?? pathFromUrl(request));
|
|
90
|
+
if (route === "publish" && publishHandler) {
|
|
91
|
+
return publishHandler.POST(request);
|
|
92
|
+
}
|
|
93
|
+
return notFound(request);
|
|
94
|
+
},
|
|
95
|
+
OPTIONS(request) {
|
|
96
|
+
// A preflight is answered the same way whatever it is preflighting, so
|
|
97
|
+
// the route is never resolved here.
|
|
98
|
+
return blocksHandler.OPTIONS(request);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -1,51 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
import type { PageDoc } from "./types.ts";
|
|
4
|
-
import { type SiteConfig } from "@avocadostudio-ai/shared";
|
|
5
|
-
export interface EditorApiHandlerConfig {
|
|
6
|
-
getPages: () => PageDoc[] | Promise<PageDoc[]>;
|
|
7
|
-
/**
|
|
8
|
-
* Optional site-config getter. When provided, `/api/editor/pages` returns
|
|
9
|
-
* `{ pages, siteConfig }` so the orchestrator's publish-diff can show
|
|
10
|
-
* header-chrome changes (name/logo/navLabels/navGroups) alongside page diffs.
|
|
11
|
-
*/
|
|
12
|
-
getSiteConfig?: () => SiteConfig | undefined | Promise<SiteConfig | undefined>;
|
|
13
|
-
getManifest?: () => BlockManifest;
|
|
14
|
-
/**
|
|
15
|
-
* Register the site's block schemas before each manifest build. Use this
|
|
16
|
-
* instead of relying on side-effect import order — Next.js's dev bundler
|
|
17
|
-
* does not reliably honor source-order side effects across RSC / SSR / API
|
|
18
|
-
* route layers, so a canonical re-registration from `@avocadostudio-ai/shared`
|
|
19
|
-
* can silently clobber the site's overrides. Called inside `getManifest`,
|
|
20
|
-
* so registration is guaranteed to be last at request time. `registerBlock`
|
|
21
|
-
* is idempotent, so repeated calls are cheap.
|
|
22
|
-
*
|
|
23
|
-
* Composes with `getManifest`: when both are set, `registerBlocks` runs
|
|
24
|
-
* first, then `getManifest` is invoked.
|
|
25
|
-
*/
|
|
26
|
-
registerBlocks?: () => void;
|
|
27
|
-
/**
|
|
28
|
-
* The block types this site renders, narrowing `/api/editor/blocks` to them.
|
|
29
|
-
*
|
|
30
|
-
* `createOrchestrator` takes the same option and declares the same catalogue,
|
|
31
|
-
* and for a while that was assumed to be enough for both routes: the
|
|
32
|
-
* declaration lives on `globalThis`, so one handler's call is visible to the
|
|
33
|
-
* other. What that reasoning missed is *when* it runs. A Next route module is
|
|
34
|
-
* evaluated on the first request to that route, so a site whose editor asks
|
|
35
|
-
* for `/api/editor/blocks` before anything has touched `/api/avocado/*` gets
|
|
36
|
-
* the manifest built before `createOrchestrator` has ever been called — and
|
|
37
|
-
* the answer is the site's own blocks plus all of Avocado's built-ins, which
|
|
38
|
-
* the site has no renderer for.
|
|
39
|
-
*
|
|
40
|
-
* Declaring it here closes that window, and passing it to both handlers is
|
|
41
|
-
* harmless: the declaration records names and the manifest is rebuilt per
|
|
42
|
-
* request.
|
|
43
|
-
*/
|
|
44
|
-
blockTypes?: readonly string[];
|
|
45
|
-
onPublish?: OnPublishFn;
|
|
46
|
-
/** Secret token required for publish requests. Checked against x-publish-token header. */
|
|
47
|
-
publishSecret?: string;
|
|
48
|
-
}
|
|
1
|
+
import type { EditorApiHandlerConfig } from "./editor-api-handler-core.ts";
|
|
2
|
+
export type { EditorApiHandlerConfig } from "./editor-api-handler-core.ts";
|
|
49
3
|
type NextRouteContext = {
|
|
50
4
|
params: Promise<{
|
|
51
5
|
path: string[];
|
|
@@ -68,10 +22,14 @@ type NextRouteContext = {
|
|
|
68
22
|
* - `/api/editor/blocks` → block manifest
|
|
69
23
|
* - `/api/editor/pages` → published pages
|
|
70
24
|
* - `/api/editor/publish` → publish content (POST)
|
|
25
|
+
*
|
|
26
|
+
* The routing, CORS, manifest and publish behaviour all live in
|
|
27
|
+
* `createEditorApiHandlerCore`, which knows nothing about Next. This wrapper
|
|
28
|
+
* adds the two things that are actually Next's: `draftMode()` and the route
|
|
29
|
+
* context holding the matched path segments.
|
|
71
30
|
*/
|
|
72
31
|
export declare function createEditorApiHandler(config: EditorApiHandlerConfig): {
|
|
73
32
|
GET: (request: Request, context: NextRouteContext) => Promise<Response>;
|
|
74
33
|
POST: (request: Request, context: NextRouteContext) => Promise<Response>;
|
|
75
34
|
OPTIONS: (request: Request, context: NextRouteContext) => Response;
|
|
76
35
|
};
|
|
77
|
-
export {};
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { applyEditorCors } from "./editor-cors.js";
|
|
4
|
-
import { checkIntegrationOnce } from "./integration-check.js";
|
|
5
|
-
import { buildBlockManifest } from "./editor-manifest.js";
|
|
6
|
-
import { declareBlockCatalogue } from "@avocadostudio-ai/shared";
|
|
1
|
+
import { createNextDraftAdapter } from "./draft-routes.js";
|
|
2
|
+
import { createEditorApiHandlerCore } from "./editor-api-handler-core.js";
|
|
7
3
|
/**
|
|
8
4
|
* Creates a single catch-all route handler that serves all editor API endpoints.
|
|
9
5
|
*
|
|
@@ -21,73 +17,23 @@ import { declareBlockCatalogue } from "@avocadostudio-ai/shared";
|
|
|
21
17
|
* - `/api/editor/blocks` → block manifest
|
|
22
18
|
* - `/api/editor/pages` → published pages
|
|
23
19
|
* - `/api/editor/publish` → publish content (POST)
|
|
20
|
+
*
|
|
21
|
+
* The routing, CORS, manifest and publish behaviour all live in
|
|
22
|
+
* `createEditorApiHandlerCore`, which knows nothing about Next. This wrapper
|
|
23
|
+
* adds the two things that are actually Next's: `draftMode()` and the route
|
|
24
|
+
* context holding the matched path segments.
|
|
24
25
|
*/
|
|
25
26
|
export function createEditorApiHandler(config) {
|
|
26
|
-
const
|
|
27
|
-
const draftDisable = createDraftDisableHandler();
|
|
28
|
-
const needsPreamble = Boolean(config.registerBlocks || config.blockTypes);
|
|
29
|
-
const manifestBuilder = needsPreamble
|
|
30
|
-
? () => {
|
|
31
|
-
config.registerBlocks?.();
|
|
32
|
-
if (config.blockTypes)
|
|
33
|
-
declareBlockCatalogue(config.blockTypes);
|
|
34
|
-
return (config.getManifest ?? buildBlockManifest)();
|
|
35
|
-
}
|
|
36
|
-
: config.getManifest;
|
|
37
|
-
const blocksHandler = createBlocksHandler(manifestBuilder ? { getManifest: manifestBuilder } : undefined);
|
|
38
|
-
const pagesHandler = createPagesHandler(config.getPages, config.getSiteConfig);
|
|
39
|
-
const publishHandler = config.onPublish
|
|
40
|
-
? createPublishHandler(config.onPublish, { publishSecret: config.publishSecret })
|
|
41
|
-
: null;
|
|
42
|
-
function matchRoute(path) {
|
|
43
|
-
const key = path.join("/");
|
|
44
|
-
if (key === "draft")
|
|
45
|
-
return "draft-enable";
|
|
46
|
-
if (key === "draft/disable")
|
|
47
|
-
return "draft-disable";
|
|
48
|
-
if (key === "blocks")
|
|
49
|
-
return "blocks";
|
|
50
|
-
if (key === "pages")
|
|
51
|
-
return "pages";
|
|
52
|
-
if (key === "publish")
|
|
53
|
-
return "publish";
|
|
54
|
-
return "not-found";
|
|
55
|
-
}
|
|
56
|
-
function notFound(request) {
|
|
57
|
-
return applyEditorCors(new Response(JSON.stringify({ error: "Not found" }), {
|
|
58
|
-
status: 404,
|
|
59
|
-
headers: { "Content-Type": "application/json" },
|
|
60
|
-
}), request.headers.get("origin"));
|
|
61
|
-
}
|
|
27
|
+
const core = createEditorApiHandlerCore({ ...config, draftAdapter: createNextDraftAdapter() });
|
|
62
28
|
return {
|
|
63
29
|
async GET(request, context) {
|
|
64
|
-
|
|
65
|
-
const { path } = await context.params;
|
|
66
|
-
const route = matchRoute(path);
|
|
67
|
-
switch (route) {
|
|
68
|
-
case "draft-enable":
|
|
69
|
-
return draftEnable(request);
|
|
70
|
-
case "draft-disable":
|
|
71
|
-
return draftDisable(request);
|
|
72
|
-
case "blocks":
|
|
73
|
-
return blocksHandler.GET(request);
|
|
74
|
-
case "pages":
|
|
75
|
-
return pagesHandler.GET(request);
|
|
76
|
-
default:
|
|
77
|
-
return notFound(request);
|
|
78
|
-
}
|
|
30
|
+
return core.GET(request, (await context.params).path);
|
|
79
31
|
},
|
|
80
32
|
async POST(request, context) {
|
|
81
|
-
|
|
82
|
-
const route = matchRoute(path);
|
|
83
|
-
if (route === "publish" && publishHandler) {
|
|
84
|
-
return publishHandler.POST(request);
|
|
85
|
-
}
|
|
86
|
-
return notFound(request);
|
|
33
|
+
return core.POST(request, (await context.params).path);
|
|
87
34
|
},
|
|
88
|
-
OPTIONS(request
|
|
89
|
-
|
|
90
|
-
return blocksHandler.OPTIONS(request);
|
|
35
|
+
OPTIONS(request) {
|
|
36
|
+
return core.OPTIONS(request);
|
|
91
37
|
},
|
|
92
38
|
};
|
|
93
39
|
}
|