@avocadostudio-ai/site-sdk 0.7.0 → 0.8.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/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 +20 -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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options shared by every host's editor rewrite.
|
|
3
|
+
*
|
|
4
|
+
* `EditorProxyOptions` in `./proxy.ts` extends this with `trailingSlash`, which
|
|
5
|
+
* exists only to re-issue a redirect Next was told to stop issuing.
|
|
6
|
+
*/
|
|
7
|
+
export type EditorRewriteOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* The internal route prefix that the dynamic editor/draft page lives under.
|
|
10
|
+
* @default "/preview-draft"
|
|
11
|
+
*/
|
|
12
|
+
previewRoute?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Query parameter that signals an editor iframe request.
|
|
15
|
+
* @default "__editor"
|
|
16
|
+
*/
|
|
17
|
+
editorParam?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Name of the cookie that keeps in-preview navigation in draft mode, or
|
|
20
|
+
* `false` to key the rewrite on {@link EditorRewriteOptions.editorParam}
|
|
21
|
+
* alone.
|
|
22
|
+
*
|
|
23
|
+
* A link clicked inside the editor iframe carries no `__editor=1`, so without
|
|
24
|
+
* a cookie the second page a user visits renders published content.
|
|
25
|
+
*
|
|
26
|
+
* The default is Next's own `__prerender_bypass`, which is not Avocado's to
|
|
27
|
+
* claim: anything else calling `draftMode().enable()` — Sanity's Presentation
|
|
28
|
+
* tool, Contentful's live preview — sets the same cookie, and every one of
|
|
29
|
+
* *their* preview requests then lands on Avocado's preview route.
|
|
30
|
+
*
|
|
31
|
+
* A host with no draft-mode convention of its own has no `__prerender_bypass`
|
|
32
|
+
* to inherit and must name and set a cookie itself; whatever it names goes
|
|
33
|
+
* here, so the rewrite triggers on the same cookie the host sets.
|
|
34
|
+
*
|
|
35
|
+
* @default "__prerender_bypass"
|
|
36
|
+
*/
|
|
37
|
+
draftCookie?: string | false;
|
|
38
|
+
};
|
|
39
|
+
/** What the host should do with this request. */
|
|
40
|
+
export type EditorRewriteDecision =
|
|
41
|
+
/** Not an editor request. The caller's cue to run its own logic, not a decision to pass through. */
|
|
42
|
+
{
|
|
43
|
+
kind: "pass";
|
|
44
|
+
}
|
|
45
|
+
/** Serve the preview route instead, with these request headers added. */
|
|
46
|
+
| {
|
|
47
|
+
kind: "preview";
|
|
48
|
+
pathname: string;
|
|
49
|
+
headers: Record<string, string>;
|
|
50
|
+
};
|
|
51
|
+
export type EditorRewriteRequest = {
|
|
52
|
+
/**
|
|
53
|
+
* The path the host routes on. Next supplies `nextUrl.pathname`, which has
|
|
54
|
+
* `basePath` already stripped; a host without the concept passes the URL's
|
|
55
|
+
* own pathname. It matters because the preview prefix is prepended to this
|
|
56
|
+
* value, and prepending it to a path that still carries a basePath produces a
|
|
57
|
+
* route that exists nowhere.
|
|
58
|
+
*/
|
|
59
|
+
pathname: string;
|
|
60
|
+
searchParams: URLSearchParams;
|
|
61
|
+
hasCookie: (name: string) => boolean;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Whether a request is the editor previewing the site, and where it should be
|
|
65
|
+
* served from if so.
|
|
66
|
+
*
|
|
67
|
+
* Pure, and the whole of the rewrite: `editorPreviewRewrite` in `./proxy.ts` is
|
|
68
|
+
* this plus `NextResponse.rewrite`, and any other host's middleware is this
|
|
69
|
+
* plus its own equivalent.
|
|
70
|
+
*/
|
|
71
|
+
export declare function decideEditorRewrite(request: EditorRewriteRequest, options?: EditorRewriteOptions): EditorRewriteDecision;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DEFAULT_PREVIEW_ROUTE } from "./editor-matcher.js";
|
|
2
|
+
import { EDITOR_RENDER_HEADER } from "./editor-render.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether a request is the editor previewing the site, and where it should be
|
|
5
|
+
* served from if so.
|
|
6
|
+
*
|
|
7
|
+
* Pure, and the whole of the rewrite: `editorPreviewRewrite` in `./proxy.ts` is
|
|
8
|
+
* this plus `NextResponse.rewrite`, and any other host's middleware is this
|
|
9
|
+
* plus its own equivalent.
|
|
10
|
+
*/
|
|
11
|
+
export function decideEditorRewrite(request, options) {
|
|
12
|
+
const previewRoute = options?.previewRoute ?? DEFAULT_PREVIEW_ROUTE;
|
|
13
|
+
const editorParam = options?.editorParam ?? "__editor";
|
|
14
|
+
const draftCookie = options?.draftCookie === undefined ? "__prerender_bypass" : options.draftCookie;
|
|
15
|
+
const isEditor = request.searchParams.get(editorParam) === "1";
|
|
16
|
+
const hasDraftCookie = draftCookie !== false && request.hasCookie(draftCookie);
|
|
17
|
+
if (!isEditor && !hasDraftCookie)
|
|
18
|
+
return { kind: "pass" };
|
|
19
|
+
/*
|
|
20
|
+
* The one fact a layout can get at.
|
|
21
|
+
*
|
|
22
|
+
* A page learns it is being previewed from `resolveEditorContext(searchParams)`.
|
|
23
|
+
* A layout receives no search params, and the root layout is exactly where a
|
|
24
|
+
* site mounts its consent banner, analytics and tag manager — so every
|
|
25
|
+
* integration loads all three into the editor iframe until somebody notices
|
|
26
|
+
* the pageviews. Stamping the rewritten request lets `isEditorRenderFrom()`
|
|
27
|
+
* answer in a layout without the site threading anything down to it.
|
|
28
|
+
*
|
|
29
|
+
* Set on the request, replacing any client-supplied header of the same name,
|
|
30
|
+
* so what a layout reads is this function's answer and not the caller's.
|
|
31
|
+
*/
|
|
32
|
+
return {
|
|
33
|
+
kind: "preview",
|
|
34
|
+
pathname: `${previewRoute}${request.pathname}`,
|
|
35
|
+
headers: { [EDITOR_RENDER_HEADER]: "1" },
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/lens/register.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { FieldTable, Primitives } from "./types.ts";
|
|
2
2
|
export type RegisterOptions = {
|
|
3
3
|
/**
|
|
4
|
-
* The CMS pack, read
|
|
4
|
+
* The CMS pack, read for its image prop naming and its row keys.
|
|
5
5
|
*
|
|
6
6
|
* Passing the same object `createLens` gets is what makes the panel and the
|
|
7
7
|
* projection agree about what an image field's two props are called. Held
|
|
8
8
|
* apart they are two declarations that must match with nothing checking that
|
|
9
9
|
* they do — and the symptom is an asset picker that never appears.
|
|
10
|
+
*
|
|
11
|
+
* `rowIdKey` and `rowTypeKey` are the same argument one level down: the
|
|
12
|
+
* projection writes a row's identity and type under those keys, so the schema
|
|
13
|
+
* has to expect them there and the list has to discriminate on them.
|
|
10
14
|
*/
|
|
11
|
-
primitives?: Pick<Primitives, "imageNaming"
|
|
15
|
+
primitives?: Pick<Primitives, "imageNaming"> & Partial<Pick<Primitives, "rowIdKey" | "rowTypeKey">>;
|
|
12
16
|
/**
|
|
13
17
|
* `false` to leave Avocado's own built-in block types in the picker.
|
|
14
18
|
*
|
|
@@ -18,12 +22,6 @@ export type RegisterOptions = {
|
|
|
18
22
|
*/
|
|
19
23
|
narrowCatalogue?: boolean;
|
|
20
24
|
};
|
|
21
|
-
/**
|
|
22
|
-
* Register every type in the table as an Avocado block type.
|
|
23
|
-
*
|
|
24
|
-
* Adding a field to a block becomes one line in one file: the schema, the
|
|
25
|
-
* panel, the projection and the merge all read the same declaration.
|
|
26
|
-
*/
|
|
27
25
|
export declare function registerFieldTable(table: FieldTable, options?: RegisterOptions): void;
|
|
28
26
|
/**
|
|
29
27
|
* The types a page body may hold, which is not every type in the table.
|
package/dist/lens/register.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `createLens` returns a value; folding a side effect into a constructor makes
|
|
13
13
|
* an object you cannot build twice, and makes the order of two imports matter.
|
|
14
14
|
*/
|
|
15
|
-
import { registerBlock, declareBlockCatalogue, z } from "@avocadostudio-ai/shared";
|
|
15
|
+
import { registerBlock, declareBlockCatalogue, z, FIELD_KINDS } from "@avocadostudio-ai/shared";
|
|
16
16
|
import { suffixNaming } from "./types.js";
|
|
17
17
|
/**
|
|
18
18
|
* A ProseMirror document, declared so the panel recognises it as one.
|
|
@@ -32,10 +32,70 @@ function humanise(key) {
|
|
|
32
32
|
.replace(/([a-z\d])([A-Z])/g, "$1 $2")
|
|
33
33
|
.replace(/^./, (c) => c.toUpperCase());
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The row types a `list: { of }` can actually be given a shape for.
|
|
37
|
+
*
|
|
38
|
+
* A named type the table does not describe is skipped rather than emitted as an
|
|
39
|
+
* empty branch: Avocado cannot project or merge a row of it — `createLens`
|
|
40
|
+
* filters those rows out — so offering it in the panel would be an Add control
|
|
41
|
+
* for something no publish could store.
|
|
42
|
+
*/
|
|
43
|
+
function rowTypesFor(of, ctx) {
|
|
44
|
+
if (!ctx.rowTypeKey)
|
|
45
|
+
return [];
|
|
46
|
+
return [...new Set(of)].filter((type) => ctx.table[type] !== undefined && !ctx.visiting.has(type));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One list row as an object schema, and `loose` is the load-bearing word.
|
|
50
|
+
*
|
|
51
|
+
* A row is a CMS document: it carries `component`, `_uid`, and every field the
|
|
52
|
+
* table chose not to declare. `z.object` strips what it does not know, and the
|
|
53
|
+
* ops engine's passthrough only covers a block's *top-level* props — so a
|
|
54
|
+
* strict row schema would silently delete the undeclared half of every row an
|
|
55
|
+
* edit touched.
|
|
56
|
+
*/
|
|
57
|
+
function rowSchema(fields, ctx, pinned) {
|
|
58
|
+
const shape = { ...pinned };
|
|
59
|
+
// Declared, not merely tolerated: an undeclared prop present in the content is
|
|
60
|
+
// an `orphan_prop` to the coverage checkers, and a row id is one per row.
|
|
61
|
+
if (ctx.rowIdKey)
|
|
62
|
+
shape[ctx.rowIdKey] = z.string().optional();
|
|
63
|
+
Object.assign(shape, shapeForFields(fields, ctx));
|
|
64
|
+
return z.object(shape).loose();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A list's schema, in the one shape the manifest can resolve.
|
|
68
|
+
*
|
|
69
|
+
* It used to be `z.array(z.record(z.string(), z.any()))` for both forms, which
|
|
70
|
+
* reaches the manifest as an array of objects with no properties — so the
|
|
71
|
+
* derivation produced no item fields at all, `resolveManifestFieldMeta` dropped
|
|
72
|
+
* the list, and the panel drew neither rows nor an Add control. The metadata
|
|
73
|
+
* registered beside it was correct the whole time; the schema is what the panel
|
|
74
|
+
* sees, and it said nothing.
|
|
75
|
+
*/
|
|
76
|
+
function listSchemaFor(spec, ctx) {
|
|
77
|
+
if ("itemFields" in spec)
|
|
78
|
+
return z.array(rowSchema(spec.itemFields, ctx, {})).optional();
|
|
79
|
+
const typeKey = ctx.rowTypeKey;
|
|
80
|
+
const types = rowTypesFor(spec.of, ctx);
|
|
81
|
+
// Nothing describable: back to the opaque array, which at least stores the
|
|
82
|
+
// rows untouched rather than rejecting them.
|
|
83
|
+
if (types.length === 0)
|
|
84
|
+
return z.array(z.record(z.string(), z.any())).optional();
|
|
85
|
+
const branches = types.map((type) => rowSchema(ctx.table[type].fields, { ...ctx, visiting: new Set([...ctx.visiting, type]) }, {
|
|
86
|
+
[typeKey]: z.literal(type)
|
|
87
|
+
}));
|
|
88
|
+
return z.array(z.discriminatedUnion(typeKey, branches)).optional();
|
|
89
|
+
}
|
|
90
|
+
function schemaForField(spec, ctx) {
|
|
36
91
|
switch (spec.kind) {
|
|
37
92
|
case "richtext":
|
|
38
93
|
return richTextSchema().optional();
|
|
94
|
+
// Stored as the string the template renders. The document only exists
|
|
95
|
+
// while the panel has it open; `richTextSchema` would also admit a doc,
|
|
96
|
+
// and a doc written back is markup the site cannot render.
|
|
97
|
+
case "html":
|
|
98
|
+
return z.string().optional();
|
|
39
99
|
case "boolean":
|
|
40
100
|
return z.boolean().optional();
|
|
41
101
|
case "number":
|
|
@@ -51,70 +111,184 @@ function schemaForField(spec) {
|
|
|
51
111
|
case "imageList":
|
|
52
112
|
return z.array(z.record(z.string(), z.any())).optional();
|
|
53
113
|
case "list":
|
|
54
|
-
return
|
|
114
|
+
return listSchemaFor(spec, ctx);
|
|
55
115
|
default:
|
|
56
116
|
return z.string().optional();
|
|
57
117
|
}
|
|
58
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* A declared field set as an object shape — a block's props, or a row's.
|
|
121
|
+
*
|
|
122
|
+
* An image is two props and `key` may be neither of them: a pack that names
|
|
123
|
+
* them `heroUrl`/`heroAlt` must not also leave a bare `hero` in the schema, or
|
|
124
|
+
* the panel draws a third control that writes nowhere.
|
|
125
|
+
*/
|
|
126
|
+
function shapeForFields(fields, ctx) {
|
|
127
|
+
const shape = {};
|
|
128
|
+
for (const [key, spec] of Object.entries(fields)) {
|
|
129
|
+
if (spec.kind === "image") {
|
|
130
|
+
shape[ctx.naming.url(key)] = z.string().optional();
|
|
131
|
+
shape[ctx.naming.alt(key)] = z.string().optional();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
shape[key] = schemaForField(spec, ctx);
|
|
135
|
+
}
|
|
136
|
+
return shape;
|
|
137
|
+
}
|
|
59
138
|
function metaForField(key, spec) {
|
|
60
139
|
const meta = {
|
|
61
|
-
|
|
140
|
+
/*
|
|
141
|
+
* `list` alone is still `text` here, and it is not a loss: a list travels
|
|
142
|
+
* the separate `listFields` channel, which carries its row shape, and the
|
|
143
|
+
* kind on this side is never read. `stringList` and `imageList` have no
|
|
144
|
+
* such channel — flattening them to `text` was the whole of AW-01, because
|
|
145
|
+
* the panel then discarded the metadata as contradicting an array schema
|
|
146
|
+
* and drew nothing.
|
|
147
|
+
*/
|
|
148
|
+
kind: spec.kind === "list" ? "text" : spec.kind,
|
|
62
149
|
label: spec.label ?? humanise(key)
|
|
63
150
|
};
|
|
64
151
|
if (spec.internal)
|
|
65
152
|
meta.internal = true;
|
|
153
|
+
if (spec.panelOnly)
|
|
154
|
+
meta.panelOnly = true;
|
|
66
155
|
if (spec.kind === "text" && spec.multiline)
|
|
67
156
|
meta.multiline = true;
|
|
68
157
|
if (spec.kind === "text" && spec.inlineEditable !== undefined)
|
|
69
158
|
meta.inlineEditable = spec.inlineEditable;
|
|
70
159
|
if (spec.kind === "richtext" && spec.inline)
|
|
71
160
|
meta.inline = true;
|
|
161
|
+
if (spec.kind === "html" && spec.inline)
|
|
162
|
+
meta.inline = true;
|
|
163
|
+
/*
|
|
164
|
+
* Not editable on the page unless the site says so.
|
|
165
|
+
*
|
|
166
|
+
* Every other kind defaults to inline-editable, and for this one that
|
|
167
|
+
* default corrupts: the overlay edits an element's text, and the value here
|
|
168
|
+
* is markup wrapped around that text. A person fixing a typo in the hero
|
|
169
|
+
* headline would write the typo back without the spans. The panel converts
|
|
170
|
+
* properly, so it is the whole of the UI until a site opts in.
|
|
171
|
+
*/
|
|
172
|
+
if (spec.kind === "html")
|
|
173
|
+
meta.inlineEditable = spec.inlineEditable ?? false;
|
|
72
174
|
if (spec.kind === "enum")
|
|
73
175
|
meta.options = [...spec.options];
|
|
74
176
|
if (spec.kind === "image" && spec.imageSpec)
|
|
75
177
|
meta.imageSpec = spec.imageSpec;
|
|
76
178
|
return meta;
|
|
77
179
|
}
|
|
180
|
+
/** The same expansion as `shapeForFields`, as panel metadata rather than schema. */
|
|
181
|
+
function metaForFields(fields, ctx) {
|
|
182
|
+
const out = {};
|
|
183
|
+
if (ctx.rowIdKey)
|
|
184
|
+
out[ctx.rowIdKey] = { kind: "text", label: humanise(ctx.rowIdKey), internal: true };
|
|
185
|
+
for (const [key, spec] of Object.entries(fields)) {
|
|
186
|
+
if (spec.kind === "image") {
|
|
187
|
+
out[ctx.naming.url(key)] = metaForField(key, spec);
|
|
188
|
+
out[ctx.naming.alt(key)] = { kind: "imageAlt", label: `${spec.label ?? humanise(key)} alt` };
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
out[key] = metaForField(key, spec);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A list's panel metadata.
|
|
197
|
+
*
|
|
198
|
+
* Declared rather than left to the derivation because the derivation cannot
|
|
199
|
+
* know all of it: an enum's options never reach the JSON schema (the schema is
|
|
200
|
+
* a plain string on purpose, see above), and neither does a label the table
|
|
201
|
+
* chose. For the `of` form this carries the per-type field sets and the
|
|
202
|
+
* discriminator the panel narrows a row by — and that the ops engine reads to
|
|
203
|
+
* refuse a new row that names no type.
|
|
204
|
+
*/
|
|
205
|
+
function listMetaFor(key, spec, ctx) {
|
|
206
|
+
const label = spec.label ?? humanise(key);
|
|
207
|
+
if ("itemFields" in spec)
|
|
208
|
+
return { label, itemFields: metaForFields(spec.itemFields, ctx) };
|
|
209
|
+
const types = rowTypesFor(spec.of, ctx);
|
|
210
|
+
if (types.length === 0)
|
|
211
|
+
return { label, itemFields: {} };
|
|
212
|
+
const itemFieldsByType = {};
|
|
213
|
+
const merged = {};
|
|
214
|
+
for (const type of types) {
|
|
215
|
+
const fields = metaForFields(ctx.table[type].fields, {
|
|
216
|
+
...ctx,
|
|
217
|
+
visiting: new Set([...ctx.visiting, type])
|
|
218
|
+
});
|
|
219
|
+
itemFieldsByType[type] = fields;
|
|
220
|
+
for (const [fieldKey, meta] of Object.entries(fields))
|
|
221
|
+
if (!(fieldKey in merged))
|
|
222
|
+
merged[fieldKey] = meta;
|
|
223
|
+
}
|
|
224
|
+
return { label, itemFields: merged, discriminator: ctx.rowTypeKey, itemFieldsByType };
|
|
225
|
+
}
|
|
78
226
|
/**
|
|
79
227
|
* Register every type in the table as an Avocado block type.
|
|
80
228
|
*
|
|
81
229
|
* Adding a field to a block becomes one line in one file: the schema, the
|
|
82
230
|
* panel, the projection and the merge all read the same declaration.
|
|
83
231
|
*/
|
|
232
|
+
/**
|
|
233
|
+
* Refuse a field this table cannot draw, rather than emitting the nearest kind.
|
|
234
|
+
*
|
|
235
|
+
* Every gap this file has produced was found by a person looking at a property
|
|
236
|
+
* panel: a control drawn for something it cannot edit, or — worse — no control
|
|
237
|
+
* at all, with the field valid in the schema and present in the codec. Nothing
|
|
238
|
+
* failed, so nothing was reported, and the integration looked finished.
|
|
239
|
+
*
|
|
240
|
+
* TypeScript already rejects an unknown `kind` for a table written in
|
|
241
|
+
* TypeScript. This is for the tables that are not: `registerFieldTable` is a
|
|
242
|
+
* published entry point, adopters call it from plain JavaScript and from JSON
|
|
243
|
+
* they generated, and a misspelled kind there is currently a field that quietly
|
|
244
|
+
* becomes a text input over a value a text input destroys.
|
|
245
|
+
*
|
|
246
|
+
* It checks the kind only. It cannot tell whether the panel's control for a
|
|
247
|
+
* kind is the *right* control — that is what `FIELD_KINDS` and the editor's own
|
|
248
|
+
* exhaustiveness check are for.
|
|
249
|
+
*/
|
|
250
|
+
function assertDrawable(type, key, spec) {
|
|
251
|
+
const kind = spec.kind;
|
|
252
|
+
if (typeof kind === "string" && KNOWN_KINDS.includes(kind))
|
|
253
|
+
return;
|
|
254
|
+
throw new Error(`registerFieldTable: ${type}.${key} declares kind ${JSON.stringify(kind)}, which is not a field kind. `
|
|
255
|
+
+ `Known kinds: ${KNOWN_KINDS.join(", ")}.`);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The kinds a table may declare.
|
|
259
|
+
*
|
|
260
|
+
* `FIELD_KINDS` is the panel's vocabulary; `list` is this layer's own, because
|
|
261
|
+
* a list is not a field — it travels the `listFields` channel instead and has
|
|
262
|
+
* no `FieldKind` spelling.
|
|
263
|
+
*/
|
|
264
|
+
const KNOWN_KINDS = [...FIELD_KINDS, "list"];
|
|
84
265
|
export function registerFieldTable(table, options) {
|
|
85
266
|
const naming = options?.primitives?.imageNaming ?? suffixNaming("", "_alt");
|
|
267
|
+
const ctx = {
|
|
268
|
+
naming,
|
|
269
|
+
table,
|
|
270
|
+
rowIdKey: options?.primitives?.rowIdKey,
|
|
271
|
+
rowTypeKey: options?.primitives?.rowTypeKey,
|
|
272
|
+
visiting: new Set()
|
|
273
|
+
};
|
|
86
274
|
for (const [type, blockSpec] of Object.entries(table)) {
|
|
87
|
-
const shape = {};
|
|
275
|
+
const shape = shapeForFields(blockSpec.fields, { ...ctx, visiting: new Set([type]) });
|
|
88
276
|
const fields = {};
|
|
89
277
|
const listFields = {};
|
|
90
278
|
for (const [key, spec] of Object.entries(blockSpec.fields)) {
|
|
91
|
-
|
|
92
|
-
if (spec.kind === "
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
itemFields
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
itemFields[naming.url(itemKey)] = metaForField(itemKey, itemSpec);
|
|
102
|
-
itemFields[naming.alt(itemKey)] = { kind: "imageAlt", label: `${itemSpec.label ?? humanise(itemKey)} alt` };
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
itemFields[itemKey] = metaForField(itemKey, itemSpec);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
listFields[key] = { label: spec.label ?? humanise(key), itemFields };
|
|
279
|
+
assertDrawable(type, key, spec);
|
|
280
|
+
if (spec.kind === "imageList") {
|
|
281
|
+
listFields[key] = {
|
|
282
|
+
label: spec.label ?? humanise(key),
|
|
283
|
+
itemFields: { image: { kind: "image", label: "Image" }, alt: { kind: "imageAlt", label: "Alt text" } }
|
|
284
|
+
};
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (spec.kind === "list") {
|
|
288
|
+
listFields[key] = listMetaFor(key, spec, { ...ctx, visiting: new Set([type]) });
|
|
109
289
|
continue;
|
|
110
290
|
}
|
|
111
291
|
if (spec.kind === "image") {
|
|
112
|
-
// An image is two props, and `key` may not be either of them: a pack
|
|
113
|
-
// that names them `heroUrl`/`heroAlt` must not also leave a bare `hero`
|
|
114
|
-
// in the schema, or the panel draws a third control that writes nowhere.
|
|
115
|
-
delete shape[key];
|
|
116
|
-
shape[naming.url(key)] = z.string().optional();
|
|
117
|
-
shape[naming.alt(key)] = z.string().optional();
|
|
118
292
|
fields[naming.url(key)] = metaForField(key, spec);
|
|
119
293
|
fields[naming.alt(key)] = { kind: "imageAlt", label: `${spec.label ?? humanise(key)} alt` };
|
|
120
294
|
continue;
|
package/dist/lens/types.d.ts
CHANGED
|
@@ -11,6 +11,20 @@ type FieldCommon = {
|
|
|
11
11
|
* keeping it out of the panel and out of the planner's view.
|
|
12
12
|
*/
|
|
13
13
|
internal?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* A person edits it, but never on the page.
|
|
16
|
+
*
|
|
17
|
+
* A section anchor, a video poster, a layout switch: real content with no
|
|
18
|
+
* element that could carry an editable marker, so the property panel is the
|
|
19
|
+
* whole of its UI. `editableCoverage` reads this as "no marker expected",
|
|
20
|
+
* which is what makes the number reachable — without it a correct
|
|
21
|
+
* integration reports 88% and the integrator gating on coverage has to guess
|
|
22
|
+
* a threshold, at which point a real regression hides behind the guess.
|
|
23
|
+
*
|
|
24
|
+
* The weaker neighbour of `internal`: that one says the field has no
|
|
25
|
+
* audience, this one says it has no element.
|
|
26
|
+
*/
|
|
27
|
+
panelOnly?: boolean;
|
|
14
28
|
/**
|
|
15
29
|
* `false` for a field with one value for every language.
|
|
16
30
|
*
|
|
@@ -52,6 +66,22 @@ export type FieldSpec =
|
|
|
52
66
|
kind: "richtext";
|
|
53
67
|
inline?: boolean;
|
|
54
68
|
})
|
|
69
|
+
/**
|
|
70
|
+
* A string of HTML — a prop the template renders with `set:html`,
|
|
71
|
+
* `dangerouslySetInnerHTML` or `v-html`.
|
|
72
|
+
*
|
|
73
|
+
* Stored as the string it is, edited as a document. Declaring one of these
|
|
74
|
+
* `richtext` is the mistake it exists to stop: `richtext` means a document,
|
|
75
|
+
* so the panel renders the markup literally and a person editing it writes
|
|
76
|
+
* broken markup back into the site's own source file. Tags outside the
|
|
77
|
+
* converter's vocabulary are preserved rather than dropped — see
|
|
78
|
+
* `@avocadostudio-ai/richtext`'s `fromHtml`.
|
|
79
|
+
*/
|
|
80
|
+
| (FieldCommon & {
|
|
81
|
+
kind: "html";
|
|
82
|
+
inline?: boolean;
|
|
83
|
+
inlineEditable?: boolean;
|
|
84
|
+
})
|
|
55
85
|
/** An image. Projects to a URL string plus a companion alt prop. */
|
|
56
86
|
| (FieldCommon & {
|
|
57
87
|
kind: "image";
|
|
@@ -99,6 +129,12 @@ export type FieldSpec =
|
|
|
99
129
|
* `of` names the child types admitted, for a CMS where each row is its own
|
|
100
130
|
* document and carries its own type; `itemFields` describes the row inline,
|
|
101
131
|
* for a CMS where the rows are plain objects. Exactly one of the two.
|
|
132
|
+
*
|
|
133
|
+
* Every type named in `of` must itself be in the table, and the pack must
|
|
134
|
+
* supply a `rowTypeKey`: the list's schema is a discriminated union over that
|
|
135
|
+
* key, with one branch per named type. A type the table does not describe is
|
|
136
|
+
* left out of the union rather than admitted as an empty shape, because
|
|
137
|
+
* `createLens` cannot project or merge a row of it either.
|
|
102
138
|
*/
|
|
103
139
|
| (FieldCommon & {
|
|
104
140
|
kind: "list";
|
package/dist/page-metadata.d.ts
CHANGED
|
@@ -64,3 +64,24 @@ export type BuildPageMetadataOptions = {
|
|
|
64
64
|
* image to put in it.
|
|
65
65
|
*/
|
|
66
66
|
export declare function buildPageMetadata(page: Pick<PageDoc, "title" | "meta" | "blocks">, options?: BuildPageMetadataOptions): PageMetadata;
|
|
67
|
+
/**
|
|
68
|
+
* Render {@link PageMetadata} as the head tags it describes.
|
|
69
|
+
*
|
|
70
|
+
* Next consumes the object directly — a route exports `metadata` and the
|
|
71
|
+
* framework emits the tags — so nothing in this repo needed this until now.
|
|
72
|
+
* Every other host writes its own `<head>`, and an integrator left to do that
|
|
73
|
+
* by hand reproduces a subset: the title and description usually, Open Graph
|
|
74
|
+
* sometimes, `twitter:card` almost never, and the canonical link not at all.
|
|
75
|
+
* That is how `test:build` came to exist — three of those omissions had already
|
|
76
|
+
* shipped from the one route that built its head by hand.
|
|
77
|
+
*
|
|
78
|
+
* Returns a string because the alternative is a shape per templating language.
|
|
79
|
+
* The values are attribute-escaped; the caller inserts it as raw HTML
|
|
80
|
+
* (`set:html` in Astro, `v-html` in Vue, `{@html}` in Svelte).
|
|
81
|
+
*
|
|
82
|
+
* `og:image` and `twitter:image` are emitted exactly as given. A relative path
|
|
83
|
+
* is legal in the document but most crawlers will not resolve it, so pass
|
|
84
|
+
* absolute URLs — the same rule that applies to `buildPageMetadata`'s
|
|
85
|
+
* `canonical`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function renderPageMetadata(metadata: PageMetadata): string;
|
package/dist/page-metadata.js
CHANGED
|
@@ -108,3 +108,67 @@ export function buildPageMetadata(page, options = {}) {
|
|
|
108
108
|
...(options.canonical ? { alternates: { canonical: options.canonical } } : {}),
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
|
+
/** `&`, `<`, `>` and `"` inside an attribute value, which a title routinely contains. */
|
|
112
|
+
function escapeAttribute(value) {
|
|
113
|
+
return value
|
|
114
|
+
.replace(/&/g, "&")
|
|
115
|
+
.replace(/</g, "<")
|
|
116
|
+
.replace(/>/g, ">")
|
|
117
|
+
.replace(/"/g, """);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Render {@link PageMetadata} as the head tags it describes.
|
|
121
|
+
*
|
|
122
|
+
* Next consumes the object directly — a route exports `metadata` and the
|
|
123
|
+
* framework emits the tags — so nothing in this repo needed this until now.
|
|
124
|
+
* Every other host writes its own `<head>`, and an integrator left to do that
|
|
125
|
+
* by hand reproduces a subset: the title and description usually, Open Graph
|
|
126
|
+
* sometimes, `twitter:card` almost never, and the canonical link not at all.
|
|
127
|
+
* That is how `test:build` came to exist — three of those omissions had already
|
|
128
|
+
* shipped from the one route that built its head by hand.
|
|
129
|
+
*
|
|
130
|
+
* Returns a string because the alternative is a shape per templating language.
|
|
131
|
+
* The values are attribute-escaped; the caller inserts it as raw HTML
|
|
132
|
+
* (`set:html` in Astro, `v-html` in Vue, `{@html}` in Svelte).
|
|
133
|
+
*
|
|
134
|
+
* `og:image` and `twitter:image` are emitted exactly as given. A relative path
|
|
135
|
+
* is legal in the document but most crawlers will not resolve it, so pass
|
|
136
|
+
* absolute URLs — the same rule that applies to `buildPageMetadata`'s
|
|
137
|
+
* `canonical`.
|
|
138
|
+
*/
|
|
139
|
+
export function renderPageMetadata(metadata) {
|
|
140
|
+
const tags = [];
|
|
141
|
+
const meta = (attr, key, value) => {
|
|
142
|
+
if (value)
|
|
143
|
+
tags.push(`<meta ${attr}="${escapeAttribute(key)}" content="${escapeAttribute(value)}" />`);
|
|
144
|
+
};
|
|
145
|
+
if (metadata.title)
|
|
146
|
+
tags.push(`<title>${escapeAttribute(metadata.title)}</title>`);
|
|
147
|
+
meta("name", "description", metadata.description);
|
|
148
|
+
if (metadata.alternates?.canonical) {
|
|
149
|
+
tags.push(`<link rel="canonical" href="${escapeAttribute(metadata.alternates.canonical)}" />`);
|
|
150
|
+
}
|
|
151
|
+
const og = metadata.openGraph;
|
|
152
|
+
if (og) {
|
|
153
|
+
meta("property", "og:title", og.title);
|
|
154
|
+
meta("property", "og:description", og.description);
|
|
155
|
+
meta("property", "og:type", og.type);
|
|
156
|
+
meta("property", "og:site_name", og.siteName);
|
|
157
|
+
meta("property", "og:url", og.url);
|
|
158
|
+
for (const image of og.images ?? [])
|
|
159
|
+
meta("property", "og:image", image);
|
|
160
|
+
}
|
|
161
|
+
const twitter = metadata.twitter;
|
|
162
|
+
if (twitter) {
|
|
163
|
+
meta("name", "twitter:card", twitter.card);
|
|
164
|
+
meta("name", "twitter:title", twitter.title);
|
|
165
|
+
meta("name", "twitter:description", twitter.description);
|
|
166
|
+
for (const image of twitter.images ?? [])
|
|
167
|
+
meta("name", "twitter:image", image);
|
|
168
|
+
}
|
|
169
|
+
if (metadata.robots) {
|
|
170
|
+
const directives = [metadata.robots.index ? "index" : "noindex", metadata.robots.follow ? "follow" : "nofollow"];
|
|
171
|
+
meta("name", "robots", directives.join(", "));
|
|
172
|
+
}
|
|
173
|
+
return tags.join("\n");
|
|
174
|
+
}
|
package/dist/proxy.d.ts
CHANGED
|
@@ -1,37 +1,16 @@
|
|
|
1
1
|
import { NextResponse, type NextRequest } from "next/server";
|
|
2
|
+
import { type EditorRewriteOptions } from "./editor-rewrite-core.ts";
|
|
2
3
|
export { DEFAULT_PREVIEW_ROUTE, buildEditorMatcher } from "./editor-matcher.ts";
|
|
4
|
+
export { decideEditorRewrite } from "./editor-rewrite-core.ts";
|
|
5
|
+
export type { EditorRewriteDecision, EditorRewriteOptions } from "./editor-rewrite-core.ts";
|
|
3
6
|
/**
|
|
4
7
|
* Options for the editor proxy factory.
|
|
8
|
+
*
|
|
9
|
+
* `previewRoute`, `editorParam` and `draftCookie` are documented on
|
|
10
|
+
* {@link EditorRewriteOptions}, which every host shares; `trailingSlash` below
|
|
11
|
+
* is Next's alone.
|
|
5
12
|
*/
|
|
6
|
-
export type EditorProxyOptions = {
|
|
7
|
-
/**
|
|
8
|
-
* The internal route prefix that the dynamic editor/draft page lives under.
|
|
9
|
-
* @default "/preview-draft"
|
|
10
|
-
*/
|
|
11
|
-
previewRoute?: string;
|
|
12
|
-
/**
|
|
13
|
-
* Query parameter that signals an editor iframe request.
|
|
14
|
-
* @default "__editor"
|
|
15
|
-
*/
|
|
16
|
-
editorParam?: string;
|
|
17
|
-
/**
|
|
18
|
-
* Name of the Next.js draft-mode bypass cookie, or `false` to ignore cookies
|
|
19
|
-
* entirely and key the rewrite on {@link EditorProxyOptions.editorParam} alone.
|
|
20
|
-
*
|
|
21
|
-
* The cookie is how navigation *inside* the editor iframe stays in draft mode:
|
|
22
|
-
* a link click carries no `__editor=1`, so without it the second page a user
|
|
23
|
-
* visits renders published content.
|
|
24
|
-
*
|
|
25
|
-
* But the cookie is Next's own, and it is not Avocado's to claim. Any other
|
|
26
|
-
* feature that calls `draftMode().enable()` sets the same
|
|
27
|
-
* `__prerender_bypass` — Sanity's Presentation tool and Contentful's live
|
|
28
|
-
* preview both do — and every one of *their* preview requests then lands on
|
|
29
|
-
* Avocado's preview route. On a site that already had Draft Mode before it had
|
|
30
|
-
* Avocado, pass `draftCookie: false` and the two stop fighting over it.
|
|
31
|
-
*
|
|
32
|
-
* @default "__prerender_bypass"
|
|
33
|
-
*/
|
|
34
|
-
draftCookie?: string | false;
|
|
13
|
+
export type EditorProxyOptions = EditorRewriteOptions & {
|
|
35
14
|
/**
|
|
36
15
|
* Set this to `true` on a site whose `next.config` sets `trailingSlash: true`.
|
|
37
16
|
*
|
|
@@ -118,4 +97,4 @@ export declare function trailingSlashRedirect(request: NextRequest): NextRespons
|
|
|
118
97
|
* The half of `createEditorProxy` a site with its own middleware needs; see
|
|
119
98
|
* {@link trailingSlashRedirect} for the composition.
|
|
120
99
|
*/
|
|
121
|
-
export declare function editorPreviewRewrite(request: NextRequest, options?:
|
|
100
|
+
export declare function editorPreviewRewrite(request: NextRequest, options?: EditorRewriteOptions): NextResponse | null;
|