@elytracms/next 0.0.52 → 0.0.54

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.
@@ -0,0 +1,86 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ /**
4
+ * `@elytracms/next/host-image` — the host image-delivery seam for DUAL-RENDERED
5
+ * project components (EC-368, from the consumer-proven EC-271 pattern).
6
+ *
7
+ * The `project.*` page-builder components are dual-rendered: the SAME module
8
+ * renders in the Vite studio canvas AND the Next host (AD-2/AD-11), so it may
9
+ * not statically import `next/image` — that would break the Vite build. And in
10
+ * the Next host these components render as Server Components, where React
11
+ * context is unavailable, so an optimized `<Image>` can't be threaded through a
12
+ * provider either.
13
+ *
14
+ * The seam that satisfies both: a tiny module singleton the host sets once at
15
+ * boot. The studio never sets it (it renders the components via the root
16
+ * `elytra.config`, which never imports the Next host), so the canvas keeps a
17
+ * plain-`<img>` fallback; the Next host injects the `ElytraImage`-backed
18
+ * renderer so content images go through `next/image`'s optimizer:
19
+ *
20
+ * ```ts
21
+ * // frontend/lib/host.ts — imported once by the catch-all route at boot
22
+ * import { elytraHostImage } from '@elytracms/next'
23
+ * import { setHostImage } from '@elytracms/next/host-image'
24
+ *
25
+ * setHostImage(elytraHostImage())
26
+ * ```
27
+ *
28
+ * WHEN (and whether) to register stays consumer-owned — legacy-interop windows
29
+ * are real: a host whose global `next/image` config is still owned by a
30
+ * previous stack (e.g. a Sanity-era custom loader) deliberately skips
31
+ * registration until the conflict clears, and its components keep the honest
32
+ * plain-`<img>` fallback.
33
+ *
34
+ * This module is deliberately its OWN subpath entry importing nothing but React
35
+ * types — safe in Vite, in RSC, and in `next.config.*`. The main
36
+ * `@elytracms/next` entry never imports it at runtime (that would bundle a
37
+ * second singleton instance into the server entry, disconnecting
38
+ * `setHostImage` from `getHostImage` — pinned by `package-build.test.ts`).
39
+ */
40
+ /** The resolved-asset slice a host image renderer needs (mirrors `ResolvedAsset`). */
41
+ interface HostImageAsset {
42
+ url: string;
43
+ width: number | null;
44
+ height: number | null;
45
+ alt: string | null;
46
+ focalPoint: {
47
+ x: number;
48
+ y: number;
49
+ } | null;
50
+ }
51
+ interface HostImageProps {
52
+ asset: HostImageAsset;
53
+ alt: string;
54
+ className?: string;
55
+ loading?: 'eager' | 'lazy';
56
+ /** Above-the-fold opt-out of lazy-loading (the hero image). */
57
+ priority?: boolean;
58
+ /** Responsive `sizes` hint so the optimizer serves a column-sized variant. */
59
+ sizes?: string;
60
+ }
61
+ type HostImageRenderer = (props: HostImageProps) => ReactNode;
62
+ /** Host-only: register the optimized image renderer (called once at host boot). */
63
+ declare function setHostImage(renderer: HostImageRenderer | null): void;
64
+ /** The registered host renderer, or `null` in the studio canvas (plain-`<img>` fallback). */
65
+ declare function getHostImage(): HostImageRenderer | null;
66
+ /**
67
+ * The `next.config.*` `images.remotePatterns` preset for Elytra asset delivery
68
+ * (EC-368 image defaults): Convex file-storage serve URLs, where uploaded
69
+ * assets live in connected mode. Without it `next/image` refuses the remote
70
+ * origin and delivery images 400 — spread it into your config:
71
+ *
72
+ * ```js
73
+ * // next.config.mjs
74
+ * import { convexAssetRemotePatterns } from '@elytracms/next/host-image'
75
+ *
76
+ * const nextConfig = {
77
+ * images: { remotePatterns: [...convexAssetRemotePatterns] },
78
+ * }
79
+ * ```
80
+ */
81
+ declare const convexAssetRemotePatterns: readonly {
82
+ protocol: 'https';
83
+ hostname: string;
84
+ }[];
85
+
86
+ export { type HostImageAsset, type HostImageProps, type HostImageRenderer, convexAssetRemotePatterns, getHostImage, setHostImage };
@@ -0,0 +1,15 @@
1
+ // src/host-image.ts
2
+ var hostImage = null;
3
+ function setHostImage(renderer) {
4
+ hostImage = renderer;
5
+ }
6
+ function getHostImage() {
7
+ return hostImage;
8
+ }
9
+ var convexAssetRemotePatterns = [
10
+ { protocol: "https", hostname: "*.convex.cloud" }
11
+ ];
12
+
13
+ export { convexAssetRemotePatterns, getHostImage, setHostImage };
14
+ //# sourceMappingURL=host-image.js.map
15
+ //# sourceMappingURL=host-image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/host-image.ts"],"names":[],"mappings":";AA8DA,IAAI,SAAA,GAAsC,IAAA;AAGnC,SAAS,aAAa,QAAA,EAA0C;AACrE,EAAA,SAAA,GAAY,QAAA;AACd;AAGO,SAAS,YAAA,GAAyC;AACvD,EAAA,OAAO,SAAA;AACT;AAiBO,IAAM,yBAAA,GAAgF;AAAA,EAC3F,EAAE,QAAA,EAAU,OAAA,EAAS,QAAA,EAAU,gBAAA;AACjC","file":"host-image.js","sourcesContent":["import type { ReactNode } from 'react'\n\n/**\n * `@elytracms/next/host-image` — the host image-delivery seam for DUAL-RENDERED\n * project components (EC-368, from the consumer-proven EC-271 pattern).\n *\n * The `project.*` page-builder components are dual-rendered: the SAME module\n * renders in the Vite studio canvas AND the Next host (AD-2/AD-11), so it may\n * not statically import `next/image` — that would break the Vite build. And in\n * the Next host these components render as Server Components, where React\n * context is unavailable, so an optimized `<Image>` can't be threaded through a\n * provider either.\n *\n * The seam that satisfies both: a tiny module singleton the host sets once at\n * boot. The studio never sets it (it renders the components via the root\n * `elytra.config`, which never imports the Next host), so the canvas keeps a\n * plain-`<img>` fallback; the Next host injects the `ElytraImage`-backed\n * renderer so content images go through `next/image`'s optimizer:\n *\n * ```ts\n * // frontend/lib/host.ts — imported once by the catch-all route at boot\n * import { elytraHostImage } from '@elytracms/next'\n * import { setHostImage } from '@elytracms/next/host-image'\n *\n * setHostImage(elytraHostImage())\n * ```\n *\n * WHEN (and whether) to register stays consumer-owned — legacy-interop windows\n * are real: a host whose global `next/image` config is still owned by a\n * previous stack (e.g. a Sanity-era custom loader) deliberately skips\n * registration until the conflict clears, and its components keep the honest\n * plain-`<img>` fallback.\n *\n * This module is deliberately its OWN subpath entry importing nothing but React\n * types — safe in Vite, in RSC, and in `next.config.*`. The main\n * `@elytracms/next` entry never imports it at runtime (that would bundle a\n * second singleton instance into the server entry, disconnecting\n * `setHostImage` from `getHostImage` — pinned by `package-build.test.ts`).\n */\n\n/** The resolved-asset slice a host image renderer needs (mirrors `ResolvedAsset`). */\nexport interface HostImageAsset {\n url: string\n width: number | null\n height: number | null\n alt: string | null\n focalPoint: { x: number; y: number } | null\n}\n\nexport interface HostImageProps {\n asset: HostImageAsset\n alt: string\n className?: string\n loading?: 'eager' | 'lazy'\n /** Above-the-fold opt-out of lazy-loading (the hero image). */\n priority?: boolean\n /** Responsive `sizes` hint so the optimizer serves a column-sized variant. */\n sizes?: string\n}\n\nexport type HostImageRenderer = (props: HostImageProps) => ReactNode\n\nlet hostImage: HostImageRenderer | null = null\n\n/** Host-only: register the optimized image renderer (called once at host boot). */\nexport function setHostImage(renderer: HostImageRenderer | null): void {\n hostImage = renderer\n}\n\n/** The registered host renderer, or `null` in the studio canvas (plain-`<img>` fallback). */\nexport function getHostImage(): HostImageRenderer | null {\n return hostImage\n}\n\n/**\n * The `next.config.*` `images.remotePatterns` preset for Elytra asset delivery\n * (EC-368 image defaults): Convex file-storage serve URLs, where uploaded\n * assets live in connected mode. Without it `next/image` refuses the remote\n * origin and delivery images 400 — spread it into your config:\n *\n * ```js\n * // next.config.mjs\n * import { convexAssetRemotePatterns } from '@elytracms/next/host-image'\n *\n * const nextConfig = {\n * images: { remotePatterns: [...convexAssetRemotePatterns] },\n * }\n * ```\n */\nexport const convexAssetRemotePatterns: readonly { protocol: 'https'; hostname: string }[] = [\n { protocol: 'https', hostname: '*.convex.cloud' },\n]\n"]}
package/dist/index.d.ts CHANGED
@@ -3,9 +3,9 @@ import { ComponentRegistry, ComponentIssue, ComponentManifest } from '@elytracms
3
3
  export { ComponentManifest, PropField, SlotSpec, defineComponent } from '@elytracms/core/component-registry';
4
4
  import { ComponentImplementations, ResolveBinding, RelationTarget } from '@elytracms/core/runtime-renderer';
5
5
  export { ComponentImplementations, RenderAsset } from '@elytracms/core/runtime-renderer';
6
- import { ResolvePageResult, ResolvedAsset, Perspective, ContentClient, CacheTags, ContentDocumentSource } from '@elytracms/core/content';
6
+ import { ResolvePageResult, ResolvedAsset, Perspective, ContentClient, CacheTags, ContentDocumentSource, ResolvedDocument } from '@elytracms/core/content';
7
7
  export { Perspective } from '@elytracms/core/content';
8
- import { LocaleConfig, Locale, RouteRecord, RedirectRecord, CollectionDef } from '@elytracms/core/cms-core';
8
+ import { LocaleConfig, Locale, RouteRecord, RedirectRecord, CollectionDef, CmsDocument } from '@elytracms/core/cms-core';
9
9
  export { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, RedirectRecord, RouteRecord, documentSchema, localeConfigSchema, redirectRecordSchema, routeRecordSchema } from '@elytracms/core/cms-core';
10
10
  import { ComponentNode } from '@elytracms/core/project-graph';
11
11
  export { ComponentNode } from '@elytracms/core/project-graph';
@@ -15,6 +15,7 @@ import { Metadata, MetadataRoute } from 'next';
15
15
  import { z } from 'zod';
16
16
  export { z } from 'zod';
17
17
  import { ImageLoader } from 'next/image';
18
+ import { HostImageRenderer } from './host-image.js';
18
19
 
19
20
  /**
20
21
  * One host-repo component: the manifest the builder edits against plus the
@@ -1035,35 +1036,167 @@ interface NextImagePrimitiveOptions {
1035
1036
  * ```
1036
1037
  */
1037
1038
  declare function nextImagePrimitive(options?: NextImagePrimitiveOptions): HostComponent;
1039
+ /** Per-deployment options for {@link elytraHostImage} (the EC-271 loader seam). */
1040
+ interface ElytraHostImageOptions {
1041
+ /** Custom `next/image` loader for non-Vercel CDNs (see {@link NextImagePrimitiveOptions}). */
1042
+ loader?: ImageLoader;
1043
+ /** Default quality (1–100) when a call site doesn't set one (Next defaults to 75). */
1044
+ quality?: number;
1045
+ }
1046
+ /**
1047
+ * The `next/image`-backed {@link HostImageRenderer} (EC-368 image defaults): the
1048
+ * renderer a Next host registers on the `@elytracms/next/host-image` bridge so
1049
+ * DUAL-RENDERED project components (which can't import `next/image` themselves)
1050
+ * deliver optimized images — resized/modern-format variants, intrinsic
1051
+ * dimensions (no layout shift), focal-point `object-position`. A dimensionless
1052
+ * record degrades to a plain `<img>` (honest, never a crash), exactly like
1053
+ * {@link ElytraImage}.
1054
+ *
1055
+ * ```ts
1056
+ * import { elytraHostImage } from '@elytracms/next'
1057
+ * import { setHostImage } from '@elytracms/next/host-image'
1058
+ *
1059
+ * setHostImage(elytraHostImage()) // once, at host boot (never in the studio)
1060
+ * ```
1061
+ */
1062
+ declare function elytraHostImage(options?: ElytraHostImageOptions): HostImageRenderer;
1038
1063
 
1039
1064
  /**
1040
- * Pure token/target logic of the draft-preview routes (EC-144), kept free of
1041
- * Next.js imports so it is unit-testable. `preview.ts` wires it into route
1042
- * handlers with `draftMode()`.
1065
+ * Env resolution for the delivery host kit (EC-368). ONE documented name per
1066
+ * concern — every kit surface reads the same resolved bag, so the two-
1067
+ * disconnected-secrets class of bug (a preview route gating on one env var
1068
+ * while the draft loader reads another) is structurally impossible: there is
1069
+ * exactly one `DRAFT_PREVIEW_TOKEN`, consumed by BOTH the preview-enable route
1070
+ * and the draft snapshot fetch.
1071
+ *
1072
+ * | Env var | Concern |
1073
+ * | --------------------- | ---------------------------------------------------- |
1074
+ * | `ELYTRA_CONVEX_URL` | Convex deployment URL — presence (with the project |
1075
+ * | | id) switches the loader to LIVE mode |
1076
+ * | `ELYTRA_PROJECT_ID` | The project the studio publishes under (= tag scope) |
1077
+ * | `DRAFT_PREVIEW_TOKEN` | The ONE draft secret: gates the preview-enable route |
1078
+ * | | AND authenticates the draft snapshot fetch (must |
1079
+ * | | equal the Convex deployment's own env var) |
1080
+ * | `REVALIDATE_SECRET` | Instant-publish webhook HMAC secret (must equal the |
1081
+ * | | deployment's `CANVAS_REVALIDATE_SECRET`) |
1082
+ * | `STUDIO_ORIGIN` | CORS origin for the studio's browser-dispatch path |
1043
1083
  */
1044
- type PreviewEvaluation = {
1045
- ok: true;
1046
- redirectTo: string;
1047
- } | {
1048
- ok: false;
1049
- status: 400 | 401;
1050
- message: string;
1084
+ /** The kit's documented env names (single source of truth, also used in docs/tests). */
1085
+ declare const ELYTRA_HOST_ENV_KEYS: {
1086
+ readonly convexUrl: "ELYTRA_CONVEX_URL";
1087
+ readonly projectId: "ELYTRA_PROJECT_ID";
1088
+ readonly draftToken: "DRAFT_PREVIEW_TOKEN";
1089
+ readonly revalidateSecret: "REVALIDATE_SECRET";
1090
+ readonly studioOrigin: "STUDIO_ORIGIN";
1051
1091
  };
1092
+ interface ElytraHostEnv {
1093
+ convexUrl?: string;
1094
+ projectId?: string;
1095
+ draftToken?: string;
1096
+ revalidateSecret?: string;
1097
+ studioOrigin?: string;
1098
+ }
1052
1099
  /**
1053
- * Only same-site path targets are accepted (`/...` but not `//host`), so the
1054
- * preview endpoint can never be used as an open redirect.
1100
+ * Resolve the kit's env bag from a raw env record (`process.env` by default at
1101
+ * the factory). Values are trimmed; empty strings resolve to `undefined` — an
1102
+ * env var set to `""` must behave exactly like an unset one (endpoints stay
1103
+ * closed, live mode stays off), never like a configured empty secret.
1055
1104
  */
1056
- declare function safeRedirectTarget(value: string | null | undefined, fallback?: string): string | null;
1105
+ declare function resolveHostEnv(env: Record<string, string | undefined>): ElytraHostEnv;
1106
+
1057
1107
  /**
1058
- * Evaluate a preview-enable request. Drafts are token-gated (EC-144
1059
- * acceptance: without the token, drafts are never reachable):
1108
+ * Site-chrome globals for delivery hosts (EC-368, from EC-369 finding #5): the
1109
+ * layout, header, and footer are NOT rendered through the canvas, so they read
1110
+ * the `settings` singleton + `navigation` documents straight from the
1111
+ * `ContentSource` and pre-resolve everything — keeping the chrome components
1112
+ * pure presentational, with no `ContentClient` or `@elytracms/*` knowledge.
1113
+ * Both real consumers hand-authored this ~200-line layer independently; the
1114
+ * MECHANISM now lives here, while the per-project projection (which settings
1115
+ * fields exist, what the chrome looks like) stays consumer-owned:
1060
1116
  *
1061
- * - an unconfigured (empty) expected token rejects every request — preview
1062
- * cannot be accidentally left open;
1063
- * - a missing or mismatched `token` query param is a 401;
1064
- * - an off-site `redirect` target is a 400.
1117
+ * - **settings**: the singleton collection's lone document (EC-274) — no
1118
+ * document id is ever named.
1119
+ * - **navigation by slot**: menus are addressed by their EC-284 alias slot
1120
+ * (`main`, `footer`), never a document id — delivery shape carries `alias`
1121
+ * since EC-364.
1122
+ * - **link resolution**: link-object values (`linkType: href | page | file`)
1123
+ * resolve through `client.resolveLink` — the same `urlForDocument` authority
1124
+ * the router serves, so a nav link to a nested page carries its FULL composed
1125
+ * path (`/krippe/tagesablauf`, EC-218/EC-360), and `home → '/'` needs no
1126
+ * special-casing.
1127
+ * - **asset resolution**: asset field values (stored ids) resolve to serve URLs
1128
+ * through the source's delivery-shaped asset list.
1129
+ *
1130
+ * Everything degrades explicitly: a missing settings document is `null`, a
1131
+ * missing slot is `null`/`[]`, an unresolvable link drops out (never a silent
1132
+ * `#`), an unknown asset id is `undefined` — the host renders sensible empties
1133
+ * for the empty-source build and surfaces gaps honestly.
1065
1134
  */
1066
- declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken: string | undefined): PreviewEvaluation;
1135
+ interface GlobalsConventions {
1136
+ /** The settings singleton's collection id. Default `'settings'`. */
1137
+ settingsCollection?: string;
1138
+ /** The navigation documents' collection id. Default `'navigation'`. */
1139
+ navigationCollection?: string;
1140
+ /**
1141
+ * Field names probed (in order) for a navigation document's entry list.
1142
+ * Default `['items', 'links']` — the two shapes the shipped consumers and the
1143
+ * starter template use.
1144
+ */
1145
+ navigationItemsFields?: readonly string[];
1146
+ }
1147
+ /** A fully-resolved link: the composed href plus the new-tab flag. */
1148
+ interface ResolvedLink {
1149
+ href: string;
1150
+ openInNewTab: boolean;
1151
+ }
1152
+ /** A leaf navigation link (children of a dropdown / footer column). */
1153
+ interface ResolvedNavLink extends ResolvedLink {
1154
+ label: string;
1155
+ /** Optional subline shown under the label (wide header dropdowns). */
1156
+ description?: string;
1157
+ }
1158
+ /** A top-level navigation entry; `href` is absent for pure group headings. */
1159
+ interface ResolvedNavEntry {
1160
+ label: string;
1161
+ href?: string;
1162
+ openInNewTab: boolean;
1163
+ description?: string;
1164
+ children: ResolvedNavLink[];
1165
+ }
1166
+ /** The resolved chrome-globals surface `loadGlobals` yields (EC-368). */
1167
+ interface SiteGlobals {
1168
+ /** The settings singleton, delivery-shaped, or `null` (empty build / not published). */
1169
+ settings: ResolvedDocument | null;
1170
+ /** The navigation document filling a slot alias (EC-284/EC-364), or `null`. */
1171
+ navigation(slot: string): ResolvedDocument | null;
1172
+ /**
1173
+ * A slot's entry list, fully resolved: labels, composed hrefs (nested paths
1174
+ * included), new-tab flags, sublines, one level of children. Entries whose
1175
+ * link cannot resolve to an href are kept only when they carry children (a
1176
+ * group heading); leaf entries without a resolvable href drop out.
1177
+ */
1178
+ navigationEntries(slot: string): ResolvedNavEntry[];
1179
+ /** Resolve a link-object field value to an href + new-tab flag, or `null`. */
1180
+ resolveLink(value: unknown): ResolvedLink | null;
1181
+ /** Resolve an asset field value (stored id / URL / `{ url }`) to a URL, or `undefined`. */
1182
+ assetUrl(value: unknown): string | undefined;
1183
+ /** The accessor client behind the globals — for bespoke projections. */
1184
+ client: ContentClient;
1185
+ /** The underlying content source (assets(), locales, routes). */
1186
+ source: ContentSource;
1187
+ }
1188
+ interface ResolveGlobalsOptions {
1189
+ /** Perspective of the chrome read. Default `'published'` (live is published-only). */
1190
+ perspective?: Perspective;
1191
+ locale?: Locale;
1192
+ conventions?: GlobalsConventions;
1193
+ }
1194
+ /**
1195
+ * Resolve the chrome globals over an already-loaded `ContentSource` — the pure
1196
+ * core of the host kit's `loadGlobals` (directly testable over fixture
1197
+ * sources).
1198
+ */
1199
+ declare function resolveGlobals(source: ContentSource, options?: ResolveGlobalsOptions): SiteGlobals;
1067
1200
 
1068
1201
  /**
1069
1202
  * Draft preview route handlers (EC-144): Next.js `draftMode()` integration.
@@ -1102,6 +1235,299 @@ declare function createPreviewRoute(options: PreviewRouteOptions): PreviewRouteH
1102
1235
  */
1103
1236
  declare function createPreviewDisableRoute(): PreviewRouteHandlers;
1104
1237
 
1238
+ /**
1239
+ * The delivery host kit (EC-368): ONE factory over (env, project config) that
1240
+ * yields every engine-shaped delivery surface a Next.js host mounts — content
1241
+ * loading (live snapshot / build-safe empty / token-gated draft), the catch-all
1242
+ * canvas route, the instant-publish revalidate webhook, draft-mode plumbing,
1243
+ * chrome globals, and the full-bleed layout hint. A consumer wires env + ~5
1244
+ * lines per surface; no engine logic is ever copied into a host repo again
1245
+ * (EC-369 findings #4/#5/#6/#11).
1246
+ *
1247
+ * ```ts
1248
+ * // frontend/lib/elytra.ts — the ONE seam file
1249
+ * import { createElytraHost } from '@elytracms/next'
1250
+ * import { collections, routes } from '../../cms'
1251
+ * import { siteComponents } from '../../components'
1252
+ *
1253
+ * export const elytra = createElytraHost({
1254
+ * slug: 'my-site',
1255
+ * locales: { default: 'en', locales: ['en'] },
1256
+ * collections,
1257
+ * routes,
1258
+ * components: siteComponents,
1259
+ * })
1260
+ * ```
1261
+ *
1262
+ * ```ts
1263
+ * // app/[[...slug]]/page.tsx
1264
+ * const route = elytra.canvasRoute()
1265
+ * export default route.Page
1266
+ * export const generateMetadata = route.generateMetadata
1267
+ *
1268
+ * // app/api/revalidate/route.ts
1269
+ * export const { POST, OPTIONS } = elytra.revalidateRoute()
1270
+ *
1271
+ * // app/api/draft/route.ts — GET /api/draft?token=…&redirect=/path
1272
+ * export const { GET } = elytra.previewRoute()
1273
+ *
1274
+ * // app/api/draft/disable/route.ts
1275
+ * export const { GET } = elytra.previewDisableRoute()
1276
+ * ```
1277
+ *
1278
+ * Trust boundaries are unchanged: live surfaces read the PUBLISHED snapshot;
1279
+ * draft is reachable only through explicit Next draft mode behind the ONE
1280
+ * `DRAFT_PREVIEW_TOKEN` (which also authenticates the draft snapshot fetch —
1281
+ * see `host-env.ts`); the revalidate webhook stays HMAC-signed. The consumer
1282
+ * keeps ownership of project config (AD-11), host components, form endpoints,
1283
+ * and anything genuinely bespoke.
1284
+ */
1285
+ interface ElytraHostOptions {
1286
+ /**
1287
+ * The project slug (config-as-code identity, mirrors `elytra.config.ts`).
1288
+ * Used as the content scope when no live backend is configured.
1289
+ */
1290
+ slug: string;
1291
+ /** Locale config — config-owned (AD-11), never read from the snapshot. */
1292
+ locales: LocaleConfig;
1293
+ /** CMS collection schema — config-owned (AD-11). */
1294
+ collections: readonly CollectionDef[];
1295
+ /** Routes as code (AD-3/AD-11): records, or the `(collections) => records` derive. */
1296
+ routes: readonly RouteRecord[] | ((collections: CollectionDef[]) => RouteRecord[]);
1297
+ /**
1298
+ * The repo's page-builder components (manifest + implementation pairs — the
1299
+ * SAME modules the studio composes with). Base primitives are always
1300
+ * included; `base.Image` is swapped for `next/image` delivery by default
1301
+ * (see `image`).
1302
+ */
1303
+ components?: readonly HostComponent[];
1304
+ /**
1305
+ * `base.Image` delivery options (loader/sizes/quality — the non-Vercel CDN
1306
+ * seam), or `false` to keep the plain-`<img>` primitive. Default: Next's own
1307
+ * optimizer, registered out of the box.
1308
+ */
1309
+ image?: NextImagePrimitiveOptions | false;
1310
+ /**
1311
+ * The public base URL of the delivery site (mirrors the config's
1312
+ * `frontendUrl`) — exposed on the handle for sitemap/canonical use.
1313
+ */
1314
+ frontendUrl?: string;
1315
+ /**
1316
+ * Env bag; defaults to `process.env`. Documented names (see `host-env.ts`):
1317
+ * `ELYTRA_CONVEX_URL`, `ELYTRA_PROJECT_ID`, `DRAFT_PREVIEW_TOKEN`,
1318
+ * `REVALIDATE_SECRET`, `STUDIO_ORIGIN`.
1319
+ */
1320
+ env?: Record<string, string | undefined>;
1321
+ /**
1322
+ * The no-backend content fallback, used whenever `ELYTRA_CONVEX_URL` +
1323
+ * `ELYTRA_PROJECT_ID` are not both set. Default: a build-safe EMPTY source —
1324
+ * consumer CI builds green with no backend, and every URL is a clean 404
1325
+ * until content exists. The starter template overrides this with its seeded
1326
+ * in-memory world.
1327
+ */
1328
+ fallbackContent?: (request: CanvasContentRequest) => Promise<ContentSource>;
1329
+ /** Chrome-globals conventions (settings/navigation collection ids, items fields). */
1330
+ globals?: GlobalsConventions;
1331
+ /** Fetch implementation override for the live snapshot loader (tests, proxies). */
1332
+ fetch?: typeof globalThis.fetch;
1333
+ }
1334
+ /** Pass-through canvas-route options the kit does not own. */
1335
+ interface HostCanvasRouteOptions {
1336
+ /** Extra binding payloads per request (see `CanvasRouteOptions.payloads`). */
1337
+ payloads?: CanvasPayloadsFactory;
1338
+ /** Tag observation hook (see `CanvasRouteOptions.onTags`). */
1339
+ onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
1340
+ /** Metadata override (see `CanvasRouteOptions.metadata`). */
1341
+ metadata?: (result: ResolvePageResult, info: CanvasRequestInfo) => Metadata;
1342
+ /**
1343
+ * Tagged data cache (instant publish). Default: enabled, scoped to the
1344
+ * kit's content scope so the studio's webhook tags match. `false` disables
1345
+ * caching entirely; an explicit `CanvasCacheOptions` overrides the scope.
1346
+ */
1347
+ cache?: CanvasCacheOptions | false;
1348
+ }
1349
+ interface LoadGlobalsOptions {
1350
+ /** Perspective of the chrome read. Default `'published'` (live is published-only). */
1351
+ perspective?: Perspective;
1352
+ }
1353
+ /** The delivery host handle {@link createElytraHost} yields. */
1354
+ interface ElytraHost {
1355
+ /**
1356
+ * The cache-tag scope: `ELYTRA_PROJECT_ID` in live mode (matching the
1357
+ * studio's webhook tags), else the config slug.
1358
+ */
1359
+ contentScope: string;
1360
+ /** The resolved env bag (documented names, trimmed, empty = unset). */
1361
+ env: ElytraHostEnv;
1362
+ /** Materialized route records (a derive is run once, at factory time). */
1363
+ routes: RouteRecord[];
1364
+ /** The registered component surface (base primitives + image swap + project components). */
1365
+ components: HostComponents;
1366
+ /** The public base URL of the delivery site, when configured. */
1367
+ frontendUrl?: string;
1368
+ /**
1369
+ * The content loader (the seam every kit surface reads through): live
1370
+ * delivery snapshot when env is configured — published, or the token-gated
1371
+ * draft for draft-mode requests — else the fallback source.
1372
+ */
1373
+ loadContent(request: CanvasContentRequest): Promise<ContentSource>;
1374
+ /**
1375
+ * Load + resolve the site-chrome globals (settings singleton, navigation by
1376
+ * slot alias, link/asset resolution — see `host-globals.ts`). One content
1377
+ * load per call; published perspective by default.
1378
+ */
1379
+ loadGlobals(options?: LoadGlobalsOptions): Promise<SiteGlobals>;
1380
+ /** The catch-all canvas route pair for `app/[[...slug]]/page.tsx`. */
1381
+ canvasRoute(options?: HostCanvasRouteOptions): CanvasRoute;
1382
+ /**
1383
+ * The instant-publish webhook handlers for `app/api/revalidate/route.ts`,
1384
+ * wired to `REVALIDATE_SECRET` (+ `STUDIO_ORIGIN` CORS). Unset secret =
1385
+ * endpoint closed (401), never a default secret.
1386
+ */
1387
+ revalidateRoute(overrides?: Partial<RevalidateRouteOptions>): RevalidateRouteHandlers;
1388
+ /**
1389
+ * The draft-mode ENABLE route for e.g. `app/api/draft/route.ts` —
1390
+ * `GET /api/draft?token=…&redirect=/path`, gated by the ONE
1391
+ * `DRAFT_PREVIEW_TOKEN` (the same secret the draft snapshot fetch sends, so
1392
+ * the two can never disconnect). Unset token = drafts unreachable.
1393
+ */
1394
+ previewRoute(): PreviewRouteHandlers;
1395
+ /** The draft-mode DISABLE route (no token — it only reduces visibility). */
1396
+ previewDisableRoute(): PreviewRouteHandlers;
1397
+ /**
1398
+ * Does the page for this catch-all `slug` OPEN full-bleed? Manifest-driven
1399
+ * (`presentation.fullBleed`, EC-368) and draft-mode aware — the host lays out
1400
+ * header clearance from data instead of a hand-kept component-id set.
1401
+ */
1402
+ pageOpensFullBleed(slug: readonly string[] | undefined): Promise<boolean>;
1403
+ }
1404
+ /** Create the delivery host handle — see the module docs for the full wiring. */
1405
+ declare function createElytraHost(options: ElytraHostOptions): ElytraHost;
1406
+
1407
+ /**
1408
+ * The LIVE half of the delivery host kit (EC-368): fetch the delivery snapshot
1409
+ * from a Convex deployment (`convex/delivery.ts` in the builder app) over
1410
+ * Convex's public HTTP query API — a plain `fetch` to
1411
+ * `POST {deployment}/api/query`, no Convex npm dependency — and assemble it
1412
+ * through the same `createStaticContentSource` path the fixture snapshot uses,
1413
+ * so accessor and rendering semantics are byte-identical across seeded, empty,
1414
+ * and live modes. This engine-izes the ~300-line `live-content.ts` both real
1415
+ * consumers hand-copied (EC-369 finding #5); the fetch + Zod-validation logic
1416
+ * now lives in exactly one place.
1417
+ *
1418
+ * Perspective handling: published requests fetch the PUBLIC `delivery:snapshot`
1419
+ * (published documents only — the standing live-is-published-only invariant).
1420
+ * Draft-mode requests fetch `delivery:draftSnapshot`, gated by the deployment's
1421
+ * `DRAFT_PREVIEW_TOKEN` — the same single secret that gates the kit's
1422
+ * preview-enable route (see `host-env.ts`).
1423
+ *
1424
+ * Routes: config-sourced route records (AD-3/AD-11, EC-207 — the host repo's
1425
+ * own `cms/`, passed via options) and the implicit per-locale home fallback are
1426
+ * MERGED (EC-177 gap 2), never XOR'd — config records win per pattern+locale.
1427
+ * The backend never carries route data.
1428
+ */
1429
+ interface LiveContentOptions {
1430
+ /** Convex deployment URL, e.g. `https://your-deployment-123.convex.cloud`. */
1431
+ convexUrl: string;
1432
+ projectId: string;
1433
+ /** Project locale config — config-owned (AD-11), never read from the snapshot. */
1434
+ locales: LocaleConfig;
1435
+ /** CMS collection schema — config-owned (AD-11); drives depth-1 relation population. */
1436
+ collections: readonly CollectionDef[];
1437
+ /** Route records — config-owned (AD-3/AD-11); merged with the home fallback. */
1438
+ routes: readonly RouteRecord[];
1439
+ /** `DRAFT_PREVIEW_TOKEN` — required for draft-perspective loads. */
1440
+ draftToken?: string;
1441
+ /** Fetch implementation override (tests, custom agents). Defaults to global fetch. */
1442
+ fetch?: typeof globalThis.fetch;
1443
+ }
1444
+ /**
1445
+ * The `delivery.ts` payload shape (validated field-by-field below). A
1446
+ * deployment IS one environment (EC-179); the snapshot carries content only
1447
+ * (EC-287) — documents, assets, editorial redirects (EC-303), and persisted
1448
+ * data-source definitions (EC-166).
1449
+ */
1450
+ interface DeliverySnapshotPayload {
1451
+ projectId: string;
1452
+ redirects?: unknown[];
1453
+ sources?: unknown;
1454
+ documents: unknown[];
1455
+ assets: unknown[];
1456
+ }
1457
+ /**
1458
+ * Implicit fallback route table (EC-187): `/` → the first `page`-collection
1459
+ * document (lowest id), once per configured locale. Merged UNDER the stored
1460
+ * records. The delivery snapshot is already perspective-filtered server-side,
1461
+ * so in draft mode this correctly previews the draft home even when
1462
+ * unpublished.
1463
+ */
1464
+ declare function fallbackHomeRoutes(documents: readonly CmsDocument[], locales: LocaleConfig): RouteRecord[];
1465
+ /**
1466
+ * Assemble a fetched delivery-snapshot payload into a `ContentSource` — the
1467
+ * pure half of {@link loadLiveContent}, exported so the parsing/assembly rules
1468
+ * are directly testable against fixture payloads (no network).
1469
+ */
1470
+ declare function assembleLiveSource(payload: DeliverySnapshotPayload, options: Pick<LiveContentOptions, 'locales' | 'collections' | 'routes'>, perspective: Perspective): ContentSource;
1471
+ /** Fetch the live delivery snapshot and assemble the `ContentSource` for one request. */
1472
+ declare function loadLiveContent(options: LiveContentOptions, perspective: Perspective): Promise<ContentSource>;
1473
+
1474
+ /**
1475
+ * Manifest-driven header clearance (EC-368): does this page OPEN full-bleed?
1476
+ *
1477
+ * A host with a floating header (occupying no flow space) must pad a page's
1478
+ * content down by the header height — unless the page's FIRST canvas block is a
1479
+ * structural hero that deliberately bleeds to the viewport top and renders
1480
+ * under the header. Which components bleed is declared on their OWN manifest
1481
+ * (`presentation: { fullBleed: true }`, `@elytracms/core/component-registry`),
1482
+ * so the host derives the decision from the registry instead of keeping a
1483
+ * parallel hand-maintained component-id set that silently drifts when a new
1484
+ * hero ships.
1485
+ *
1486
+ * Pure over the delivery-shaped document: the composition field is found by
1487
+ * VALUE (the same `compositionNodesOf` selection the renderer uses), so hosts
1488
+ * with `body`, `content`, or bespoke composition field names all work. Absent
1489
+ * document, empty composition, unknown component, or an undeclared hint all
1490
+ * answer `false` — the safe default is clearance.
1491
+ *
1492
+ * ```tsx
1493
+ * const outcome = client.resolvePage(path)
1494
+ * const fullBleed = firstBlockFullBleed(outcome.document, components.registry)
1495
+ * return fullBleed ? page : <div className="pt-[var(--header-clearance)]">{page}</div>
1496
+ * ```
1497
+ *
1498
+ * The host kit wires the resolution half for you: `elytra.pageOpensFullBleed(slug)`.
1499
+ */
1500
+ declare function firstBlockFullBleed(document: Record<string, unknown> | null | undefined, registry: ComponentRegistry): boolean;
1501
+
1502
+ /**
1503
+ * Pure token/target logic of the draft-preview routes (EC-144), kept free of
1504
+ * Next.js imports so it is unit-testable. `preview.ts` wires it into route
1505
+ * handlers with `draftMode()`.
1506
+ */
1507
+ type PreviewEvaluation = {
1508
+ ok: true;
1509
+ redirectTo: string;
1510
+ } | {
1511
+ ok: false;
1512
+ status: 400 | 401;
1513
+ message: string;
1514
+ };
1515
+ /**
1516
+ * Only same-site path targets are accepted (`/...` but not `//host`), so the
1517
+ * preview endpoint can never be used as an open redirect.
1518
+ */
1519
+ declare function safeRedirectTarget(value: string | null | undefined, fallback?: string): string | null;
1520
+ /**
1521
+ * Evaluate a preview-enable request. Drafts are token-gated (EC-144
1522
+ * acceptance: without the token, drafts are never reachable):
1523
+ *
1524
+ * - an unconfigured (empty) expected token rejects every request — preview
1525
+ * cannot be accidentally left open;
1526
+ * - a missing or mismatched `token` query param is a 401;
1527
+ * - an off-site `redirect` target is a 400.
1528
+ */
1529
+ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken: string | undefined): PreviewEvaluation;
1530
+
1105
1531
  /**
1106
1532
  * @elytracms/next — the embedded runtime (EC-144, vision AD-1): render
1107
1533
  * builder-managed pages inside the user's own Next.js App Router repo with
@@ -1114,4 +1540,4 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
1114
1540
  */
1115
1541
  declare const PACKAGE = "@elytracms/next";
1116
1542
 
1117
- export { type AssetImageLoaderOptions, type BindingPayloads, type CacheWrapper, type CacheWrapperOptions, type CachedEntry, type CanvasCacheOptions, type CanvasContentRequest, type CanvasDataSource, type CanvasPageOutcome, type CanvasPayloadContext, type CanvasPayloadsFactory, CanvasRenderer, type CanvasRendererProps, type CanvasRequestInfo, type CanvasRoute, type CanvasRouteOptions, type CanvasRouteProps, type CanvasSitemapOptions, type CanvasSitemapResult, type ContentSnapshot, type ContentSnapshotOptions, type ContentSource, type CreateCanvasSitemapOptions, type DefineHostComponentsOptions, ElytraImage, type ElytraImageAsset, type ElytraImageProps, type HostComponent, type HostComponents, type MaterializeSourcePayloadsInput, type MaterializedSourcePayloads, PACKAGE, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, REVALIDATE_SIGNATURE_HEADER, type ResolveCanvasPageOptions, type RevalidateEvaluation, type RevalidatePayload, type RevalidateRouteHandlers, type RevalidateRouteOptions, type SitemapParamsProvider, type SitemapRouteContext, type SitemapSkippedRoute, type SourcePayloadError, type StaticContentData, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, evaluatePreviewRequest, evaluateRevalidateRequest, isSourcePayloadError, loadCachedEntry, materializeSourcePayloads, mergeCacheTags, mergeRouteRecords, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError, splitLocalePath };
1543
+ export { type AssetImageLoaderOptions, type BindingPayloads, type CacheWrapper, type CacheWrapperOptions, type CachedEntry, type CanvasCacheOptions, type CanvasContentRequest, type CanvasDataSource, type CanvasPageOutcome, type CanvasPayloadContext, type CanvasPayloadsFactory, CanvasRenderer, type CanvasRendererProps, type CanvasRequestInfo, type CanvasRoute, type CanvasRouteOptions, type CanvasRouteProps, type CanvasSitemapOptions, type CanvasSitemapResult, type ContentSnapshot, type ContentSnapshotOptions, type ContentSource, type CreateCanvasSitemapOptions, type DefineHostComponentsOptions, ELYTRA_HOST_ENV_KEYS, type ElytraHost, type ElytraHostEnv, type ElytraHostImageOptions, type ElytraHostOptions, ElytraImage, type ElytraImageAsset, type ElytraImageProps, type GlobalsConventions, type HostCanvasRouteOptions, type HostComponent, type HostComponents, type LiveContentOptions, type LoadGlobalsOptions, type MaterializeSourcePayloadsInput, type MaterializedSourcePayloads, type NextImagePrimitiveOptions, PACKAGE, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, REVALIDATE_SIGNATURE_HEADER, type ResolveCanvasPageOptions, type ResolveGlobalsOptions, type ResolvedLink, type ResolvedNavEntry, type ResolvedNavLink, type RevalidateEvaluation, type RevalidatePayload, type RevalidateRouteHandlers, type RevalidateRouteOptions, type SiteGlobals, type SitemapParamsProvider, type SitemapRouteContext, type SitemapSkippedRoute, type SourcePayloadError, type StaticContentData, assembleLiveSource, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, fallbackHomeRoutes, firstBlockFullBleed, isSourcePayloadError, loadCachedEntry, loadLiveContent, materializeSourcePayloads, mergeCacheTags, mergeRouteRecords, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError, splitLocalePath };