smoodly 0.0.34 → 0.0.36
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 +807 -1326
- package/dist/admin/drift.js +3 -2
- package/dist/admin/editor/EditorView.js +10 -3
- package/dist/admin/editor/SeoSettings.d.ts +7 -1
- package/dist/admin/editor/SeoSettings.js +2 -2
- package/dist/admin/editor/autosave.d.ts +6 -1
- package/dist/admin/editor/autosave.js +43 -3
- package/dist/admin/field-label.d.ts +2 -0
- package/dist/admin/field-label.js +2 -0
- package/dist/admin/forms/FieldWidget.js +2 -2
- package/dist/admin/forms/list.js +10 -1
- package/dist/admin/next/create-admin.d.ts +4 -4
- package/dist/admin/next/create-admin.js +5 -5
- package/dist/admin/next/index.d.ts +1 -1
- package/dist/admin/serialize.d.ts +8 -1
- package/dist/admin/serialize.js +23 -7
- package/dist/admin/shell/AdminApp.d.ts +3 -3
- package/dist/admin/shell/AdminApp.js +2 -2
- package/dist/admin/shell/EntryForm.js +11 -3
- package/dist/admin/shell/GlobalForm.js +3 -2
- package/dist/admin/shell/presence-realtime.d.ts +2 -2
- package/dist/admin/shell/router.d.ts +5 -0
- package/dist/admin/shell/router.js +13 -0
- package/dist/cli/space-io.js +1 -1
- package/dist/collections.d.ts +4 -0
- package/dist/collections.js +9 -1
- package/dist/entry-page.d.ts +12 -10
- package/dist/entry-store.d.ts +29 -0
- package/dist/entry-store.js +53 -0
- package/dist/env.d.ts +5 -5
- package/dist/env.js +2 -2
- package/dist/fields.d.ts +13 -7
- package/dist/fields.js +2 -2
- package/dist/global.d.ts +4 -7
- package/dist/index.d.ts +10 -39
- package/dist/index.js +1 -1
- package/dist/migrations.js +1 -1
- package/dist/next/canvas-slot.d.ts +7 -0
- package/dist/next/canvas-slot.js +9 -0
- package/dist/next/index.d.ts +3 -1
- package/dist/next/index.js +3 -1
- package/dist/next/meta.d.ts +6 -4
- package/dist/next/meta.js +14 -17
- package/dist/next/page-renderer.d.ts +2 -0
- package/dist/next/page-renderer.js +18 -11
- package/dist/next/preview-pill.d.ts +9 -0
- package/dist/next/preview-pill.js +56 -0
- package/dist/next/preview-route.d.ts +1 -0
- package/dist/next/preview-route.js +30 -0
- package/dist/next/queries.d.ts +10 -11
- package/dist/next/queries.js +7 -2
- package/dist/page.d.ts +5 -2
- package/dist/page.js +3 -1
- package/dist/pairing.d.ts +10 -10
- package/dist/path-index.d.ts +3 -0
- package/dist/path-index.js +7 -0
- package/dist/read-shapes.d.ts +64 -0
- package/dist/read-shapes.js +52 -0
- package/dist/render.d.ts +3 -3
- package/dist/resolve.d.ts +12 -0
- package/dist/resolve.js +29 -7
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +1 -1
- package/dist/schema.d.ts +22 -1
- package/dist/seo.d.ts +18 -0
- package/dist/seo.js +53 -0
- package/dist/site.d.ts +15 -16
- package/dist/site.js +64 -58
- package/dist/sql-space.d.ts +15 -0
- package/dist/sql-space.js +54 -39
- package/dist/sql.d.ts +11 -1
- package/dist/sql.js +78 -25
- package/dist/supabase-entry-store.d.ts +3 -1
- package/dist/supabase-entry-store.js +39 -0
- package/dist/supabase-path-index.d.ts +1 -0
- package/dist/supabase-path-index.js +16 -0
- package/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/package.json +1 -5
package/dist/entry-page.d.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import type { ComponentType, ReactElement } from "react";
|
|
2
2
|
import type { CollectionSchema } from "./collections.ts";
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
3
|
+
import type { EntryItem } from "./read-shapes.ts";
|
|
4
|
+
import type { Materialize } from "./schema.ts";
|
|
5
5
|
export type EntryPageSchema = {
|
|
6
6
|
kind: "entryPage";
|
|
7
7
|
name: string;
|
|
8
8
|
collection: string;
|
|
9
9
|
};
|
|
10
|
-
/** What an entry page renders: the entry
|
|
11
|
-
*
|
|
12
|
-
export type EntryPageProps = {
|
|
13
|
-
entry:
|
|
14
|
-
path: string;
|
|
15
|
-
locale: string;
|
|
16
|
-
locales: Record<string, LocaleAlternate | null>;
|
|
10
|
+
/** What an entry page renders: the entry as a read returns it — its path, locale and
|
|
11
|
+
* locale alternates ride on it (spec 2026-09-29 content API contract §3). */
|
|
12
|
+
export type EntryPageProps<F = Record<string, unknown>> = {
|
|
13
|
+
entry: EntryItem<F>;
|
|
17
14
|
};
|
|
18
15
|
export type PairedEntryPage = ((props: EntryPageProps) => ReactElement) & {
|
|
19
16
|
schema: EntryPageSchema;
|
|
20
17
|
};
|
|
21
|
-
|
|
18
|
+
/** The collection's fields as a read returns them. */
|
|
19
|
+
type Materialized<C extends CollectionSchema<any>> = {
|
|
20
|
+
[K in keyof NonNullable<C["__entry"]>]: Materialize<NonNullable<C["__entry"]>[K]>;
|
|
21
|
+
};
|
|
22
|
+
export declare function entryPage<C extends CollectionSchema<any>>(collection: C, View: ComponentType<EntryPageProps<Materialized<C>>>): PairedEntryPage;
|
|
23
|
+
export {};
|
package/dist/entry-store.d.ts
CHANGED
|
@@ -81,6 +81,22 @@ export type ListOptions = {
|
|
|
81
81
|
*/
|
|
82
82
|
parent?: string | null;
|
|
83
83
|
};
|
|
84
|
+
export type SnapshotQuery = {
|
|
85
|
+
locale: string;
|
|
86
|
+
/** true: the live row (the editor preview); false: the published snapshot. */
|
|
87
|
+
draft: boolean;
|
|
88
|
+
where?: Where;
|
|
89
|
+
order?: EntryOrder;
|
|
90
|
+
limit?: number;
|
|
91
|
+
parent?: string | null;
|
|
92
|
+
/** Only these entries (getEntry by id). */
|
|
93
|
+
ids?: string[];
|
|
94
|
+
};
|
|
95
|
+
/** `fields` is the merged snapshot (shared, localized, `slug`) of the version shown; `record` never leaves the site layer. */
|
|
96
|
+
export type SnapshotRow = {
|
|
97
|
+
record: EntryRecord;
|
|
98
|
+
fields: Record<string, unknown>;
|
|
99
|
+
};
|
|
84
100
|
export type CreateEntryOptions = {
|
|
85
101
|
locale: string;
|
|
86
102
|
parentId?: string | null;
|
|
@@ -102,6 +118,11 @@ export interface EntryStore {
|
|
|
102
118
|
create(collection: string, fields: Record<string, unknown>, options: CreateEntryOptions, by?: string): Promise<EntryRecord>;
|
|
103
119
|
get(collection: string, id: string): Promise<EntryRecord | null>;
|
|
104
120
|
list(collection: string, options?: ListOptions): Promise<EntryRecord[]>;
|
|
121
|
+
/** Filters, orders and limits over the version SHOWN: the published snapshot (a row
|
|
122
|
+
* without one falls back to the live merge), or the live row when `draft` (spec
|
|
123
|
+
* 2026-09-29 content API contract §5). Only rows present in `locale`; the published
|
|
124
|
+
* path also needs the row published. */
|
|
125
|
+
query(collection: string, q: SnapshotQuery): Promise<SnapshotRow[]>;
|
|
105
126
|
/** Every entry row, registered collection or not, with each published locale's live fields (spec 2026-09-27 editor safety §3). */
|
|
106
127
|
scanEntries(): Promise<EntryScan[]>;
|
|
107
128
|
/** Batched read for the resolver: the MERGED fields in `locale`, keyed
|
|
@@ -168,6 +189,8 @@ export interface EntryStore {
|
|
|
168
189
|
}[]>;
|
|
169
190
|
resolvePath(locale: string, path: string): Promise<PathHit | null>;
|
|
170
191
|
pathsOf(collection: string, id: string): Promise<Record<string, string>>;
|
|
192
|
+
/** id → full live path in `locale` for many entries of one collection; ids without one are absent. */
|
|
193
|
+
pathsIn(collection: string, ids: string[], locale: string): Promise<Record<string, string>>;
|
|
171
194
|
/** locale → this entry's old addresses. */
|
|
172
195
|
redirectsOf(collection: string, id: string): Promise<Record<string, string[]>>;
|
|
173
196
|
}
|
|
@@ -239,6 +262,10 @@ export declare function entryTitleIn(record: EntryRecord, locale: string, schema
|
|
|
239
262
|
* follow-ups for the mixed-type and JSON-null edges.
|
|
240
263
|
*/
|
|
241
264
|
export declare function compareEntries(order: EntryOrder, seq: (e: EntryRecord) => number): (a: EntryRecord, b: EntryRecord) => number;
|
|
265
|
+
/** Comparator for `query`: like compareEntries, but a declared field reads the
|
|
266
|
+
* merged snapshot, `updatedAt` the locale row's, and an absent, null or empty-string value
|
|
267
|
+
* sorts last in both directions; ties break by creation order (ascending). */
|
|
268
|
+
export declare function compareSnapshots(order: EntryOrder, locale: string, seq: (id: string) => number): (a: SnapshotRow, b: SnapshotRow) => number;
|
|
242
269
|
export declare class MemoryEntryStore implements EntryStore {
|
|
243
270
|
private refs;
|
|
244
271
|
private entries;
|
|
@@ -287,6 +314,7 @@ export declare class MemoryEntryStore implements EntryStore {
|
|
|
287
314
|
create(collection: string, fields: Record<string, unknown>, options: CreateEntryOptions, by?: string): Promise<EntryRecord>;
|
|
288
315
|
get(collection: string, id: string): Promise<EntryRecord | null>;
|
|
289
316
|
list(collection: string, options?: ListOptions): Promise<EntryRecord[]>;
|
|
317
|
+
query(collection: string, q: SnapshotQuery): Promise<SnapshotRow[]>;
|
|
290
318
|
getMany(collection: string, ids: string[], locale: string, options?: {
|
|
291
319
|
status?: "published";
|
|
292
320
|
}): Promise<Record<string, Record<string, unknown>>>;
|
|
@@ -315,6 +343,7 @@ export declare class MemoryEntryStore implements EntryStore {
|
|
|
315
343
|
sourceId: string;
|
|
316
344
|
}[]>;
|
|
317
345
|
resolvePath(locale: string, path: string): Promise<PathHit | null>;
|
|
346
|
+
pathsIn(collection: string, ids: string[], locale: string): Promise<Record<string, string>>;
|
|
318
347
|
pathsOf(collection: string, id: string): Promise<{}>;
|
|
319
348
|
redirectsOf(collection: string, id: string): Promise<Record<string, string[]>>;
|
|
320
349
|
}
|
package/dist/entry-store.js
CHANGED
|
@@ -172,6 +172,32 @@ export function compareEntries(order, seq) {
|
|
|
172
172
|
return d !== 0 ? d * dir : (seq(a) - seq(b)) * dir;
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
|
+
/** Comparator for `query`: like compareEntries, but a declared field reads the
|
|
176
|
+
* merged snapshot, `updatedAt` the locale row's, and an absent, null or empty-string value
|
|
177
|
+
* sorts last in both directions; ties break by creation order (ascending). */
|
|
178
|
+
export function compareSnapshots(order, locale, seq) {
|
|
179
|
+
return (a, b) => {
|
|
180
|
+
if (order === "manual") {
|
|
181
|
+
const d = (a.record.sort ?? Infinity) - (b.record.sort ?? Infinity);
|
|
182
|
+
return d !== 0 ? d : seq(a.record.id) - seq(b.record.id);
|
|
183
|
+
}
|
|
184
|
+
const dir = order.direction === "asc" ? 1 : -1;
|
|
185
|
+
const value = (r) => order.by === "createdAt" ? r.record.createdAt : order.by === "updatedAt" ? r.record.locales[locale]?.updatedAt : r.fields[order.by];
|
|
186
|
+
const empty = (v) => (v === "" ? undefined : v); // a cleared text or date is empty too
|
|
187
|
+
const va = empty(value(a)), vb = empty(value(b));
|
|
188
|
+
const tie = seq(a.record.id) - seq(b.record.id);
|
|
189
|
+
if (order.by === "createdAt")
|
|
190
|
+
return tie * dir; // the clock can tie within a millisecond; creation order cannot
|
|
191
|
+
if (va == null && vb == null)
|
|
192
|
+
return tie;
|
|
193
|
+
if (va == null)
|
|
194
|
+
return 1;
|
|
195
|
+
if (vb == null)
|
|
196
|
+
return -1;
|
|
197
|
+
const d = typeof va === "number" && typeof vb === "number" ? va - vb : String(va).localeCompare(String(vb));
|
|
198
|
+
return d !== 0 ? d * dir : tie;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
175
201
|
export class MemoryEntryStore {
|
|
176
202
|
constructor(collections,
|
|
177
203
|
// pass the SAME index to MemoryPageStore so cross-kind questions work
|
|
@@ -389,6 +415,29 @@ export class MemoryEntryStore {
|
|
|
389
415
|
rows = rows.slice(0, options.limit);
|
|
390
416
|
return rows.map((e) => this.snapshot(e));
|
|
391
417
|
}
|
|
418
|
+
async query(collection, q) {
|
|
419
|
+
const schema = this.schema(collection);
|
|
420
|
+
const wanted = q.ids ? new Set(q.ids) : null;
|
|
421
|
+
const rows = [];
|
|
422
|
+
for (const e of this.entries.values()) {
|
|
423
|
+
if (e.collection !== collection)
|
|
424
|
+
continue;
|
|
425
|
+
if (wanted && !wanted.has(e.id))
|
|
426
|
+
continue;
|
|
427
|
+
if (q.parent !== undefined && e.parentId !== q.parent)
|
|
428
|
+
continue;
|
|
429
|
+
const row = e.locales[q.locale];
|
|
430
|
+
if (!row || (!q.draft && row.status !== "published"))
|
|
431
|
+
continue;
|
|
432
|
+
const snapshot = !q.draft && row.publishedVersionId ? this.versions.get(row.publishedVersionId) : undefined;
|
|
433
|
+
const fields = snapshot ? structuredClone(snapshot.fields) : entryFieldsIn(e, q.locale);
|
|
434
|
+
if (q.where && !matchesWhere(fields, q.where))
|
|
435
|
+
continue;
|
|
436
|
+
rows.push({ record: this.snapshot(e), fields });
|
|
437
|
+
}
|
|
438
|
+
rows.sort(compareSnapshots(q.order ?? collectionOrder(schema), q.locale, (id) => this.order.get(id)));
|
|
439
|
+
return typeof q.limit === "number" ? rows.slice(0, q.limit) : rows;
|
|
440
|
+
}
|
|
392
441
|
async getMany(collection, ids, locale, options) {
|
|
393
442
|
this.schema(collection);
|
|
394
443
|
const out = {};
|
|
@@ -644,6 +693,10 @@ export class MemoryEntryStore {
|
|
|
644
693
|
resolvePath(locale, path) {
|
|
645
694
|
return this.paths.resolve(locale, path);
|
|
646
695
|
}
|
|
696
|
+
pathsIn(collection, ids, locale) {
|
|
697
|
+
this.schema(collection);
|
|
698
|
+
return this.paths.livePathsIn("entry", ids, locale);
|
|
699
|
+
}
|
|
647
700
|
pathsOf(collection, id) {
|
|
648
701
|
return this.row(collection, id) ? this.paths.pathsOf({ kind: "entry", id }) : Promise.resolve({});
|
|
649
702
|
}
|
package/dist/env.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type SmoodlyEnv = {
|
|
|
4
4
|
url: string;
|
|
5
5
|
/** Server-only: the service-role key (self-host) or the environment's write key (cloud). */
|
|
6
6
|
secretKey: string;
|
|
7
|
-
/** What a browser may hold for the content project: anon/publishable key, or the
|
|
7
|
+
/** What a browser may hold for the content project: anon/publishable key, or the cloud's per-environment key. It reads no content (sign-in and presence only). */
|
|
8
8
|
publishableKey: string;
|
|
9
9
|
/** Where editors sign in. Defaults to `url`. */
|
|
10
10
|
authUrl: string;
|
|
@@ -15,12 +15,12 @@ export declare function resolveSmoodlyEnv(env: Record<string, string | undefined
|
|
|
15
15
|
/** `resolveSmoodlyEnv(process.env)` — call lazily, never at module load,
|
|
16
16
|
* so `next build` without env still succeeds. */
|
|
17
17
|
export declare const smoodlyEnv: () => SmoodlyEnv;
|
|
18
|
-
/** What the browser needs to join the content project's
|
|
19
|
-
export type
|
|
18
|
+
/** What the browser needs to join the admin's presence channel: the content project's URL, publishable key and space, public values only — never for content reads (spec 2026-09-29 content API contract §8). */
|
|
19
|
+
export type PresenceConfig = {
|
|
20
20
|
url: string;
|
|
21
21
|
key: string;
|
|
22
22
|
spaceId: string;
|
|
23
23
|
};
|
|
24
|
-
/** The
|
|
24
|
+
/** The presence config, with the space the database says the secret key acts in —
|
|
25
25
|
* asked, never read out of the key, so it cannot disagree with where rows land. */
|
|
26
|
-
export declare function
|
|
26
|
+
export declare function presenceConfig(env: SmoodlyEnv, db?: Parameters<typeof readCurrentSpace>[0]): Promise<PresenceConfig>;
|
package/dist/env.js
CHANGED
|
@@ -25,9 +25,9 @@ export const smoodlyEnv = () => resolveSmoodlyEnv(process.env);
|
|
|
25
25
|
/** One lookup per process per content project and key: the space never
|
|
26
26
|
* changes for a key. A failed lookup is not cached, so the next call retries. */
|
|
27
27
|
const spaces = new Map();
|
|
28
|
-
/** The
|
|
28
|
+
/** The presence config, with the space the database says the secret key acts in —
|
|
29
29
|
* asked, never read out of the key, so it cannot disagree with where rows land. */
|
|
30
|
-
export function
|
|
30
|
+
export function presenceConfig(env, db) {
|
|
31
31
|
const cacheKey = `${env.url}\n${env.secretKey}`;
|
|
32
32
|
let space = spaces.get(cacheKey);
|
|
33
33
|
if (!space) {
|
package/dist/fields.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ export declare function normalizeOptions<V extends string>(options: readonly Opt
|
|
|
24
24
|
export declare function optionsOf(options: unknown): Option[];
|
|
25
25
|
/** The values a stored choice may take. */
|
|
26
26
|
export declare const optionValues: (options: unknown) => string[];
|
|
27
|
+
/** A ref field's value at the type level: the target and how deep the ref site resolves it.
|
|
28
|
+
* Never a runtime value — `Values<M>` turns it into what a read returns. */
|
|
29
|
+
export type RefMode = "shallow" | "full" | (string & {});
|
|
30
|
+
export type RefTo<T, M extends RefMode = "shallow"> = {
|
|
31
|
+
readonly __refTo: T;
|
|
32
|
+
readonly __mode: M;
|
|
33
|
+
};
|
|
34
|
+
type Rebase<V, M extends RefMode> = V extends RefTo<infer T, any> ? RefTo<T, M> : V extends RefTo<infer T, any>[] ? RefTo<T, M>[] : V;
|
|
27
35
|
export type FieldBuilder<Value = unknown, Flags extends BuilderFlags = {}> = {
|
|
28
36
|
readonly descriptor: Descriptor;
|
|
29
37
|
optional(): FieldBuilder<Value, Flags & {
|
|
@@ -43,7 +51,9 @@ export type FieldBuilder<Value = unknown, Flags extends BuilderFlags = {}> = {
|
|
|
43
51
|
* the named refs, one level; object values nest deeper
|
|
44
52
|
* - no call — hydrate the entry itself; its refs stay ids
|
|
45
53
|
*/
|
|
46
|
-
resolve(
|
|
54
|
+
resolve(): FieldBuilder<Rebase<Value, "full">, Flags>;
|
|
55
|
+
resolve<const K extends string>(...keys: K[]): FieldBuilder<Rebase<Value, K>, Flags>;
|
|
56
|
+
resolve<const P extends Record<string, unknown>>(projection: P): FieldBuilder<Rebase<Value, keyof P & string>, Flags>;
|
|
47
57
|
/** Hide the field while the condition holds (spec 2026-09-14 §12): it leaves the form and validation skips it; the stored value is kept. */
|
|
48
58
|
hidden(condition: Condition): FieldBuilder<Value, Flags>;
|
|
49
59
|
/** phantom, type-level only */
|
|
@@ -105,14 +115,10 @@ export declare const f: {
|
|
|
105
115
|
* 300px sidebar cannot show one legibly (spec 2026-09-07, list field §1).
|
|
106
116
|
* `.min`/`.max` count items. */
|
|
107
117
|
list: <B extends FieldBuilder<any, any>>(item: B & NotLocalized<B>) => FieldBuilder<ValueOf<B>[], {}>;
|
|
108
|
-
ref: <T>(target: () => T) => FieldBuilder<
|
|
109
|
-
refList: <T>(target: () => T) => FieldBuilder<
|
|
118
|
+
ref: <T>(target: () => T) => FieldBuilder<RefTo<T, "shallow">, {}>;
|
|
119
|
+
refList: <T>(target: () => T) => FieldBuilder<RefTo<T, "shallow">[], {}>;
|
|
110
120
|
};
|
|
111
121
|
export type ValueOf<B> = B extends FieldBuilder<infer V, infer Flags> ? Flags extends {
|
|
112
122
|
optional: true;
|
|
113
123
|
} ? V | undefined : V : never;
|
|
114
|
-
/** Resolved entry for a ref target — refined when collections land. */
|
|
115
|
-
export type Resolved<T> = T extends {
|
|
116
|
-
__entry?: infer E;
|
|
117
|
-
} ? E : Record<string, unknown>;
|
|
118
124
|
export {};
|
package/dist/fields.js
CHANGED
|
@@ -37,13 +37,13 @@ function builder(descriptor) {
|
|
|
37
37
|
max: (n) => chain({ max: n }),
|
|
38
38
|
min: (n) => chain({ min: n }),
|
|
39
39
|
default: (v) => chain({ default: v }),
|
|
40
|
-
resolve: (...projection) => {
|
|
40
|
+
resolve: ((...projection) => {
|
|
41
41
|
if (projection.length === 0)
|
|
42
42
|
return chain({ resolve: "full" });
|
|
43
43
|
if (typeof projection[0] === "object")
|
|
44
44
|
return chain({ resolve: projection[0] });
|
|
45
45
|
return chain({ resolve: Object.fromEntries(projection.map((k) => [k, true])) });
|
|
46
|
-
},
|
|
46
|
+
}),
|
|
47
47
|
hidden: (condition) => chain({ hidden: condition }),
|
|
48
48
|
};
|
|
49
49
|
}
|
package/dist/global.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CollectionSchema } from "./collections.ts";
|
|
2
2
|
import type { Descriptor } from "./fields.ts";
|
|
3
3
|
import type { PairedComponent } from "./pairing.tsx";
|
|
4
|
-
import type { BuilderMap,
|
|
5
|
-
import type {
|
|
4
|
+
import type { BuilderMap, SectionSchema, Values } from "./schema.ts";
|
|
5
|
+
import type { GlobalItem, VisualGlobalItem } from "./read-shapes.ts";
|
|
6
6
|
/** The reserved tree node type of a placement. */
|
|
7
7
|
export declare const GLOBAL_NODE_TYPE = "global";
|
|
8
8
|
/** The reserved keys a visual item's settings, styles steps and css live under in its row (spec 2026-09-14 §8). */
|
|
@@ -46,12 +46,9 @@ type GlobalInput<N extends string = string> = {
|
|
|
46
46
|
export declare function globalItem<F extends BuilderMap, const N extends string = string>(input: GlobalInput<N> & {
|
|
47
47
|
fields: F;
|
|
48
48
|
section?: undefined;
|
|
49
|
-
}): GlobalSchema<
|
|
49
|
+
}): GlobalSchema<GlobalItem<Values<F>>, N>;
|
|
50
50
|
export declare function globalItem<F extends BuilderMap, St extends BuilderMap, const N extends string = string>(input: GlobalInput<N> & {
|
|
51
51
|
section: PairedComponent<SectionSchema<F, St>, F, St>;
|
|
52
52
|
fields?: undefined;
|
|
53
|
-
}): GlobalSchema<
|
|
54
|
-
settings: NamespaceValues<St>;
|
|
55
|
-
styles: StyleProps;
|
|
56
|
-
}, N>;
|
|
53
|
+
}): GlobalSchema<VisualGlobalItem<Values<F>, Values<St>>, N>;
|
|
57
54
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type { PathIndex } from "./path-index.ts";
|
|
|
51
51
|
export { MemoryEntryStore } from "./entry-store.ts";
|
|
52
52
|
export { SupabaseEntryStore } from "./supabase-entry-store.ts";
|
|
53
53
|
export { SupabasePathIndex } from "./supabase-path-index.ts";
|
|
54
|
-
export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryScan, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, } from "./entry-store.ts";
|
|
54
|
+
export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryScan, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, SnapshotQuery, SnapshotRow, } from "./entry-store.ts";
|
|
55
55
|
export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.ts";
|
|
56
56
|
export { affectedTargets, pageTag, entryTag, globalTag, pagesTreeTag, collectionTag } from "./revalidate.ts";
|
|
57
57
|
export type { AffectedTargets } from "./revalidate.ts";
|
|
@@ -66,7 +66,10 @@ export type { PageStore, PageRecord, PageLocale, PageVersion, PageScan, PageStor
|
|
|
66
66
|
export { pathOptions, pageErrors, localeRow, pageRefEdges, DEFAULT_LOCALES } from "./store.ts";
|
|
67
67
|
export type { SmoodlyConfig, ResolvedConfig } from "./config.ts";
|
|
68
68
|
export type { FieldBuilder, Descriptor, ValueOf, Option, OptionInput, OptionValue } from "./fields.ts";
|
|
69
|
-
export type { SectionSchema, ElementSchema, BuilderMap } from "./schema.ts";
|
|
69
|
+
export type { SectionSchema, ElementSchema, BuilderMap, Values } from "./schema.ts";
|
|
70
|
+
export type { CollectionSeo } from "./seo.ts";
|
|
71
|
+
export type { EntryRef, EntryItem, PageItem, GlobalItem, VisualGlobalItem, Seo } from "./read-shapes.ts";
|
|
72
|
+
export type { RefTo } from "./fields.ts";
|
|
70
73
|
export type { Props, PairedComponent } from "./pairing.tsx";
|
|
71
74
|
export type { CollectionSchema, EntryOrder } from "./collections.ts";
|
|
72
75
|
export { DEFAULT_TOOLSET, type Toolset, type ToolsetMark, type ToolsetNode } from "./toolset.ts";
|
|
@@ -94,23 +97,7 @@ export declare const smoodly: {
|
|
|
94
97
|
omit: string[];
|
|
95
98
|
};
|
|
96
99
|
css?: false;
|
|
97
|
-
sample?: Partial<{ [
|
|
98
|
-
__flags?: infer Fl;
|
|
99
|
-
} ? Fl extends {
|
|
100
|
-
optional: true;
|
|
101
|
-
} ? K : never : never; }[keyof F]>]: import("./fields.ts").ValueOf<F[K_1]>; } & { [K_2 in { [K in keyof F]: F[K] extends {
|
|
102
|
-
__flags?: infer Fl;
|
|
103
|
-
} ? Fl extends {
|
|
104
|
-
optional: true;
|
|
105
|
-
} ? K : never : never; }[keyof F]]?: import("./fields.ts").ValueOf<F[K_2]> | undefined; } & { [K_4 in Exclude<keyof S, { [K_3 in keyof S]: S[K_3] extends {
|
|
106
|
-
__flags?: infer Fl;
|
|
107
|
-
} ? Fl extends {
|
|
108
|
-
optional: true;
|
|
109
|
-
} ? K_3 : never : never; }[keyof S]>]: import("./fields.ts").ValueOf<S[K_4]>; } & { [K_5 in { [K_3 in keyof S]: S[K_3] extends {
|
|
110
|
-
__flags?: infer Fl;
|
|
111
|
-
} ? Fl extends {
|
|
112
|
-
optional: true;
|
|
113
|
-
} ? K_3 : never : never; }[keyof S]]?: import("./fields.ts").ValueOf<S[K_5]> | undefined; }> | undefined;
|
|
100
|
+
sample?: Partial<{ [K in keyof import("./schema.ts").NamespaceValues<F>]: import("./schema.ts").NamespaceValues<F>[K] extends import("./fields.ts").RefTo<any, any> ? string : import("./schema.ts").NamespaceValues<F>[K] extends import("./fields.ts").RefTo<any, any>[] ? string[] : import("./schema.ts").NamespaceValues<F>[K]; } & { [K_1 in keyof import("./schema.ts").NamespaceValues<S>]: import("./schema.ts").NamespaceValues<S>[K_1] extends import("./fields.ts").RefTo<any, any> ? string : import("./schema.ts").NamespaceValues<S>[K_1] extends import("./fields.ts").RefTo<any, any>[] ? string[] : import("./schema.ts").NamespaceValues<S>[K_1]; }> | undefined;
|
|
114
101
|
}): import("./schema.ts").SectionSchema<F, S>;
|
|
115
102
|
element<F extends import("./schema.ts").BuilderMap = {}, S extends import("./schema.ts").BuilderMap = {}>(input: {
|
|
116
103
|
name: string;
|
|
@@ -124,23 +111,7 @@ export declare const smoodly: {
|
|
|
124
111
|
omit: string[];
|
|
125
112
|
};
|
|
126
113
|
css?: false;
|
|
127
|
-
sample?: Partial<{ [
|
|
128
|
-
__flags?: infer Fl;
|
|
129
|
-
} ? Fl extends {
|
|
130
|
-
optional: true;
|
|
131
|
-
} ? K : never : never; }[keyof F]>]: import("./fields.ts").ValueOf<F[K_1]>; } & { [K_2 in { [K in keyof F]: F[K] extends {
|
|
132
|
-
__flags?: infer Fl;
|
|
133
|
-
} ? Fl extends {
|
|
134
|
-
optional: true;
|
|
135
|
-
} ? K : never : never; }[keyof F]]?: import("./fields.ts").ValueOf<F[K_2]> | undefined; } & { [K_4 in Exclude<keyof S, { [K_3 in keyof S]: S[K_3] extends {
|
|
136
|
-
__flags?: infer Fl;
|
|
137
|
-
} ? Fl extends {
|
|
138
|
-
optional: true;
|
|
139
|
-
} ? K_3 : never : never; }[keyof S]>]: import("./fields.ts").ValueOf<S[K_4]>; } & { [K_5 in { [K_3 in keyof S]: S[K_3] extends {
|
|
140
|
-
__flags?: infer Fl;
|
|
141
|
-
} ? Fl extends {
|
|
142
|
-
optional: true;
|
|
143
|
-
} ? K_3 : never : never; }[keyof S]]?: import("./fields.ts").ValueOf<S[K_5]> | undefined; }> | undefined;
|
|
114
|
+
sample?: Partial<{ [K in keyof import("./schema.ts").NamespaceValues<F>]: import("./schema.ts").NamespaceValues<F>[K] extends import("./fields.ts").RefTo<any, any> ? string : import("./schema.ts").NamespaceValues<F>[K] extends import("./fields.ts").RefTo<any, any>[] ? string[] : import("./schema.ts").NamespaceValues<F>[K]; } & { [K_1 in keyof import("./schema.ts").NamespaceValues<S>]: import("./schema.ts").NamespaceValues<S>[K_1] extends import("./fields.ts").RefTo<any, any> ? string : import("./schema.ts").NamespaceValues<S>[K_1] extends import("./fields.ts").RefTo<any, any>[] ? string[] : import("./schema.ts").NamespaceValues<S>[K_1]; }> | undefined;
|
|
144
115
|
} & {
|
|
145
116
|
contexts: ("richtext" | "column")[];
|
|
146
117
|
}): import("./schema.ts").ElementSchema<F, S>;
|
|
@@ -157,8 +128,8 @@ export declare const smoodly: {
|
|
|
157
128
|
};
|
|
158
129
|
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
|
|
159
130
|
export type { MarkNode, MarkPath, Marks, Scope } from "./marks.ts";
|
|
160
|
-
export { resolveSmoodlyEnv, smoodlyEnv,
|
|
161
|
-
export type { SmoodlyEnv,
|
|
131
|
+
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig } from "./env.ts";
|
|
132
|
+
export type { SmoodlyEnv, PresenceConfig } from "./env.ts";
|
|
162
133
|
export { clientOptions, createSmoodlyClient } from "./client.ts";
|
|
163
134
|
export { createSmoodlySite, pageContextOf } from "./site.ts";
|
|
164
|
-
export type { SiteOptions,
|
|
135
|
+
export type { SiteOptions, SitePageSchema, SmoodlySite, LoadOptions, PageTreeItem, EntryTreeItem, EntryQuery, } from "./site.ts";
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,6 @@ export const smoodly = {
|
|
|
66
66
|
marks,
|
|
67
67
|
};
|
|
68
68
|
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.js";
|
|
69
|
-
export { resolveSmoodlyEnv, smoodlyEnv,
|
|
69
|
+
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig } from "./env.js";
|
|
70
70
|
export { clientOptions, createSmoodlyClient } from "./client.js";
|
|
71
71
|
export { createSmoodlySite, pageContextOf } from "./site.js";
|
package/dist/migrations.js
CHANGED
|
@@ -43,7 +43,7 @@ export function viewsFile(collections, existing, at) {
|
|
|
43
43
|
const changed = collections.filter((c) => existing.get(c.name) !== collectionHash(c.sql));
|
|
44
44
|
if (changed.length === 0)
|
|
45
45
|
return null;
|
|
46
|
-
const text = "-- Smoodly: read views
|
|
46
|
+
const text = "-- Smoodly: read views for this app's collections, written by `npx smoodly migrate`\n" +
|
|
47
47
|
`-- (${at.toISOString()}). One block per collection; the marker's hash is the block's SQL, compared on the next run.\n` +
|
|
48
48
|
"-- Never edit a written file: change the collection and run migrate again.\n\n" +
|
|
49
49
|
changed.map((c) => `${collectionMarker(c)}\n${c.sql.trim()}`).join("\n\n") +
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// The one Smoodly client module a live page carries (spec 2026-09-29
|
|
3
|
+
// pre-install fixes §1). Next ships every client module a route's server
|
|
4
|
+
// graph imports, rendered or not; this stub is that module, and the bridge
|
|
5
|
+
// and the canvas net load from it lazily — only when the renderer is in
|
|
6
|
+
// editor mode, so a live page downloads neither.
|
|
7
|
+
import dynamic from "next/dynamic";
|
|
8
|
+
export const CanvasBridgeSlot = dynamic(() => import("../admin/next/bridge.js").then((m) => m.EditorBridge));
|
|
9
|
+
export const CanvasNetSlot = dynamic(() => import("./canvas-boundary.js").then((m) => m.CanvasBoundary));
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { createPageRenderer } from "./page-renderer.tsx";
|
|
2
2
|
export type { PageRendererOptions, PathRenderOptions, PageRegistration } from "./page-renderer.tsx";
|
|
3
|
-
export { metadataFrom } from "./meta.ts";
|
|
3
|
+
export { metadataFrom, metadataFromSeo } from "./meta.ts";
|
|
4
4
|
export { createSmoodlyQueries } from "./queries.ts";
|
|
5
5
|
export type { SmoodlyQueries, QueryOptions, EntriesQuery, TypedEntryItem, GlobalValue } from "./queries.ts";
|
|
6
|
+
export { previewRoute } from "./preview-route.ts";
|
|
7
|
+
export { previewPill } from "./preview-pill.tsx";
|
package/dist/next/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { createPageRenderer } from "./page-renderer.js";
|
|
2
|
-
export { metadataFrom } from "./meta.js";
|
|
2
|
+
export { metadataFrom, metadataFromSeo } from "./meta.js";
|
|
3
3
|
export { createSmoodlyQueries } from "./queries.js";
|
|
4
|
+
export { previewRoute } from "./preview-route.js";
|
|
5
|
+
export { previewPill } from "./preview-pill.js";
|
package/dist/next/meta.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* layout
|
|
2
|
+
import type { Seo } from "../read-shapes.ts";
|
|
3
|
+
/** Maps the effective SEO values onto Next metadata (spec 2026-09-10 page
|
|
4
|
+
* fields §4, 2026-09-29 content API contract §7). Absent values leave the
|
|
5
|
+
* app's layout defaults untouched: no seo at all returns {}. */
|
|
6
|
+
export declare function metadataFromSeo(seo: Seo | undefined): Metadata;
|
|
7
|
+
/** A page tree's stored meta as Next metadata; the SEO title falls back to the page's own title. */
|
|
6
8
|
export declare function metadataFrom(meta: unknown, page?: {
|
|
7
9
|
title?: string;
|
|
8
10
|
}): Metadata;
|
package/dist/next/meta.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { imageUrl } from "../image/loaders.js";
|
|
2
|
-
|
|
3
|
-
/** Maps the
|
|
4
|
-
* fields §4
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const m = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : {};
|
|
9
|
-
const str = (key) => (typeof m[key] === "string" && m[key] ? m[key] : undefined);
|
|
10
|
-
const title = str("title") ?? (page?.title ? page.title : undefined);
|
|
11
|
-
const description = str("description");
|
|
12
|
-
const ogImage = m.ogImage;
|
|
2
|
+
import { pageSeo } from "../seo.js";
|
|
3
|
+
/** Maps the effective SEO values onto Next metadata (spec 2026-09-10 page
|
|
4
|
+
* fields §4, 2026-09-29 content API contract §7). Absent values leave the
|
|
5
|
+
* app's layout defaults untouched: no seo at all returns {}. */
|
|
6
|
+
export function metadataFromSeo(seo) {
|
|
7
|
+
const { title, description, image, noindex } = seo ?? {};
|
|
13
8
|
const out = {};
|
|
14
9
|
if (title)
|
|
15
10
|
out.title = title;
|
|
@@ -20,16 +15,18 @@ export function metadataFrom(meta, page) {
|
|
|
20
15
|
og.title = title;
|
|
21
16
|
if (description)
|
|
22
17
|
og.description = description;
|
|
23
|
-
if (
|
|
18
|
+
if (image) {
|
|
24
19
|
// The delivery seam decides the URL; the cloud's transform loader (OQ 28) can serve a real 1200-wide render.
|
|
25
|
-
og.images = [{ url: imageUrl(
|
|
26
|
-
}
|
|
27
|
-
else if (typeof ogImage === "string" && ogImage) {
|
|
28
|
-
og.images = [ogImage];
|
|
20
|
+
og.images = [{ url: imageUrl(image, { width: 1200 }), width: image.width, height: image.height, ...(image.alt ? { alt: image.alt } : {}) }];
|
|
29
21
|
}
|
|
30
22
|
if (Object.keys(og).length > 0)
|
|
31
23
|
out.openGraph = og;
|
|
32
|
-
if (
|
|
24
|
+
if (noindex === true)
|
|
33
25
|
out.robots = { index: false, follow: false };
|
|
34
26
|
return out;
|
|
35
27
|
}
|
|
28
|
+
/** A page tree's stored meta as Next metadata; the SEO title falls back to the page's own title. */
|
|
29
|
+
export function metadataFrom(meta, page) {
|
|
30
|
+
const m = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : {};
|
|
31
|
+
return metadataFromSeo(pageSeo(m, page?.title ?? ""));
|
|
32
|
+
}
|
|
@@ -30,6 +30,8 @@ export type PageRendererOptions = {
|
|
|
30
30
|
/** Locale-relative CMS path → site URL. Default: the path itself. With
|
|
31
31
|
* next-intl: `(path, locale) => getPathname({ locale, href: path })`. */
|
|
32
32
|
href?: (path: string, locale: string) => string;
|
|
33
|
+
/** Where the preview pill's Exit goes (spec 2026-09-29 pre-install fixes §2): the route that exports `previewRoute`. Default `/api/preview`. */
|
|
34
|
+
previewRoute?: string;
|
|
33
35
|
};
|
|
34
36
|
export type PathRenderOptions = {
|
|
35
37
|
locale?: string;
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// The site-route seam (spec 2026-09-05 §5): the two route files call
|
|
3
3
|
// renderSmoodlyPath; everything else — request gating, draft mode, the
|
|
4
|
-
// cache keyed by (locale, path) and tagged by id, 404s, the editor
|
|
5
|
-
// bridge — lives here, over the
|
|
6
|
-
// the app, exactly like
|
|
4
|
+
// cache keyed by (locale, path, target) and tagged by id, 404s, the editor
|
|
5
|
+
// bridge (through the lazy canvas stub) — lives here, over the
|
|
6
|
+
// framework-free site layer. Bound once by the app, exactly like
|
|
7
|
+
// createAdminOpHandler.
|
|
7
8
|
import { Suspense } from "react";
|
|
8
9
|
import { unstable_cache } from "next/cache";
|
|
9
10
|
import { draftMode } from "next/headers";
|
|
10
11
|
import { notFound, permanentRedirect, unstable_rethrow } from "next/navigation";
|
|
11
12
|
import { connection } from "next/server";
|
|
12
|
-
import {
|
|
13
|
-
import { CanvasBoundary } from "./canvas-boundary.js";
|
|
13
|
+
import { CanvasBridgeSlot, CanvasNetSlot } from "./canvas-slot.js";
|
|
14
14
|
import { DRAFT_META_NAME } from "../admin/editor/canvas-diagnosis.js";
|
|
15
15
|
import { fixedPathFor } from "../localize.js";
|
|
16
16
|
import { decodeSegment, joinPath } from "../paths.js";
|
|
17
17
|
import { renderPage } from "../render.js";
|
|
18
18
|
import { entryTag, pageTag, globalTag } from "../revalidate.js";
|
|
19
|
+
import { toEntryItem } from "../read-shapes.js";
|
|
19
20
|
import { pageContextOf } from "../site.js";
|
|
20
|
-
import {
|
|
21
|
+
import { metadataFromSeo } from "./meta.js";
|
|
22
|
+
import { previewPill } from "./preview-pill.js";
|
|
21
23
|
/** Logged once per renderer when Next's cacheComponents is on (spec 2026-09-27 cache components §2). */
|
|
22
24
|
export const CACHE_COMPONENTS_WARNING = "smoodly: Next's cacheComponents is on, and Smoodly does not support it — a missing page answers 200 with noindex instead of 404, and a redirect answers 200 instead of 308. Turn cacheComponents off in next.config.ts; the Next.js guide's \"Cache Components\" section says why (https://smoodly.io/docs/frameworks/nextjs).";
|
|
23
25
|
/** The canvas net (spec 2026-09-27 section guard §5): Server Components hand a section that failed to the
|
|
24
26
|
* browser, where CanvasBoundary catches it. Defined here, not in the client file: a function exported
|
|
25
27
|
* from a "use client" module is a reference on the server and cannot be called. */
|
|
26
|
-
const canvasWrap = (child, section) => (_jsx(Suspense, { fallback: null, children: _jsx(
|
|
28
|
+
const canvasWrap = (child, section) => (_jsx(Suspense, { fallback: null, children: _jsx(CanvasNetSlot, { id: section.id, title: section.title, children: child }) }));
|
|
27
29
|
export function createPageRenderer(options) {
|
|
28
30
|
const { site } = options;
|
|
29
31
|
const href = options.href ?? ((path) => path);
|
|
@@ -34,7 +36,10 @@ export function createPageRenderer(options) {
|
|
|
34
36
|
async function load(target, locale, path, draft, version) {
|
|
35
37
|
if (draft)
|
|
36
38
|
return site.load(target, locale, path, { draft: true, ...(version !== undefined ? { version } : {}) });
|
|
37
|
-
|
|
39
|
+
// Keyed by what the unit holds, not only where: an address that changes
|
|
40
|
+
// owner — a deploy handing it to a code record, which fires no tag —
|
|
41
|
+
// can never serve the previous occupant (spec 2026-09-29 pre-install fixes §5).
|
|
42
|
+
return unstable_cache(() => site.load(target, locale, path), ["smoodly-site", locale, path, target.kind, target.id], {
|
|
38
43
|
tags: [target.kind === "page" ? pageTag(target.id) : entryTag(target.id)],
|
|
39
44
|
})();
|
|
40
45
|
}
|
|
@@ -69,7 +74,7 @@ export function createPageRenderer(options) {
|
|
|
69
74
|
console.warn(`smoodly: no entry page for collection "${hit.collection}" — the entry at ${hit.path} is a 404.`);
|
|
70
75
|
notFound();
|
|
71
76
|
}
|
|
72
|
-
return _jsx(View, { entry: hit.entry,
|
|
77
|
+
return _jsx(View, { entry: toEntryItem(hit.entry, hit.entry.fields, hit) });
|
|
73
78
|
}
|
|
74
79
|
/** hreflang from the SAME map a language switch reads (spec 2026-09-15 §3): the locales with a URL, through href. */
|
|
75
80
|
function alternates(hit) {
|
|
@@ -115,7 +120,7 @@ export function createPageRenderer(options) {
|
|
|
115
120
|
// The draft marker tells the page editor's canvas diagnosis that Smoodly
|
|
116
121
|
// rendered this response (spec 2026-09-27 shadowed-page detector §3);
|
|
117
122
|
// React hoists the meta into the head. Never outside draft mode.
|
|
118
|
-
return (_jsxs(_Fragment, { children: [draft ? _jsx("meta", { name: DRAFT_META_NAME }) : null, editor ? _jsx(
|
|
123
|
+
return (_jsxs(_Fragment, { children: [draft ? _jsx("meta", { name: DRAFT_META_NAME }) : null, editor ? _jsx(CanvasBridgeSlot, {}) : null, draft && !editor && !version ? previewPill({ route: options.previewRoute }) : null, render(hit, editor, locale)] }));
|
|
119
124
|
}
|
|
120
125
|
async function metadata(pathFor, opts, accept) {
|
|
121
126
|
// generateMetadata runs alongside the page render, so it gates too.
|
|
@@ -129,7 +134,9 @@ export function createPageRenderer(options) {
|
|
|
129
134
|
const hit = await load(target, locale, path, draft);
|
|
130
135
|
if (!hit || !accept(hit))
|
|
131
136
|
return {};
|
|
132
|
-
|
|
137
|
+
// A page type without seo: true still gets its page title as <title>; seo: true adds the panel's values on top.
|
|
138
|
+
const seo = hit.seo ?? (hit.kind === "page" ? { title: hit.title } : undefined);
|
|
139
|
+
return { ...metadataFromSeo(seo), ...alternates(hit) };
|
|
133
140
|
}
|
|
134
141
|
const any = () => true;
|
|
135
142
|
/** The escape hatch may only render ITS OWN registration's record. */
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
/** The pill's whole client side. `closed` lives in the closure, so a hidden
|
|
3
|
+
* pill stays hidden across client navigations and returns on a reload. */
|
|
4
|
+
export declare const PREVIEW_PILL_SCRIPT: string;
|
|
5
|
+
/** The pill for a route that does not go through the renderer: render it
|
|
6
|
+
* when `(await draftMode()).isEnabled`, never in the editor's canvas. */
|
|
7
|
+
export declare function previewPill(opts?: {
|
|
8
|
+
route?: string;
|
|
9
|
+
}): ReactElement;
|