@elytracms/next 0.0.81 → 0.0.83
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/index.d.ts +295 -97
- package/dist/index.js +107 -59
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ComponentType, ReactNode } from 'react';
|
|
2
2
|
import { ComponentRegistry, ComponentIssue, ComponentManifest } from '@elytracms/core/component-registry';
|
|
3
3
|
export { ComponentManifest, PropField, SlotSpec, defineComponent } from '@elytracms/core/component-registry';
|
|
4
|
-
import { ComponentImplementations, ResolveBinding, RelationTarget } from '@elytracms/core/runtime-renderer';
|
|
4
|
+
import { ComponentImplementations, ResolveBinding, RelationTarget, DocumentBodyItem } from '@elytracms/core/runtime-renderer';
|
|
5
5
|
export { ComponentImplementations, RenderAsset } from '@elytracms/core/runtime-renderer';
|
|
6
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,
|
|
9
|
-
export { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, RedirectRecord, RouteRecord, documentSchema, fallbackHomeRoutes, localeConfigSchema, mergeRouteRecords, redirectRecordSchema, routeRecordSchema } from '@elytracms/core/cms-core';
|
|
8
|
+
import { LocaleConfig, Locale, RouteRecord, CollectionDef, RedirectRecord } from '@elytracms/core/cms-core';
|
|
9
|
+
export { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, LocaleUrlPath, RedirectRecord, RouteRecord, documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, redirectRecordSchema, routeRecordSchema, splitLocalePath } from '@elytracms/core/cms-core';
|
|
10
10
|
import { ComponentNode } from '@elytracms/core/project-graph';
|
|
11
11
|
export { ComponentNode } from '@elytracms/core/project-graph';
|
|
12
12
|
import { AssetRecord, PersistenceAdapter } from '@elytracms/core/persistence';
|
|
@@ -16,6 +16,7 @@ import { z } from 'zod';
|
|
|
16
16
|
export { z } from 'zod';
|
|
17
17
|
import { ImageLoader } from 'next/image';
|
|
18
18
|
import { HostImageRenderer } from './host-image.js';
|
|
19
|
+
import { HostComponentEntry } from '@elytracms/core/studio-core';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* One host-repo component: the manifest the builder edits against plus the
|
|
@@ -169,6 +170,29 @@ interface CanvasRendererProps {
|
|
|
169
170
|
* issue (EC-290), never a silent `#`.
|
|
170
171
|
*/
|
|
171
172
|
links?: Record<string, string>;
|
|
173
|
+
/**
|
|
174
|
+
* The resolved document's collection-declared delivery presentation (EC-499,
|
|
175
|
+
* from the render outcome): which field(s) hold the body. Absent → the EC-458
|
|
176
|
+
* shape picker selects the body by VALUE, exactly as before.
|
|
177
|
+
*/
|
|
178
|
+
presentation?: DeliveryPresentation;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A collection's `presentation` declaration (EC-499), resolved for delivery: the
|
|
182
|
+
* body field list normalized from `string | string[]`. Plain data — it rides the
|
|
183
|
+
* (serializable) render outcome and the RSC boundary like the baked
|
|
184
|
+
* assets/relations/links do.
|
|
185
|
+
*
|
|
186
|
+
* EC-524: this renderer applies no CHROME. It carried a `wrapper` component id
|
|
187
|
+
* briefly (never released); a detail wrapper is a page template, templates are
|
|
188
|
+
* code, and the host now supplies one from its own route file via
|
|
189
|
+
* `createCanvasRoute`'s `wrap` option. What stays here is content selection —
|
|
190
|
+
* which is exactly what makes a canvas body and a rich-text body render
|
|
191
|
+
* agnostically through the one entry point.
|
|
192
|
+
*/
|
|
193
|
+
interface DeliveryPresentation {
|
|
194
|
+
/** Field names whose values form the body, in render order. */
|
|
195
|
+
body?: readonly string[];
|
|
172
196
|
}
|
|
173
197
|
/** Default payloads: the route-bound document and the dynamic route params. */
|
|
174
198
|
declare function defaultBindingPayloads(result: ResolvePageResult): BindingPayloads;
|
|
@@ -178,6 +202,15 @@ declare function defaultBindingPayloads(result: ResolvePageResult): BindingPaylo
|
|
|
178
202
|
* the `ContentSource`).
|
|
179
203
|
*/
|
|
180
204
|
declare function CanvasRenderer(props: CanvasRendererProps): Promise<ReactNode>;
|
|
205
|
+
/**
|
|
206
|
+
* The ordered body items of a resolved document (EC-499): the values of the
|
|
207
|
+
* collection's declared `presentation.body` fields concatenated in declaration
|
|
208
|
+
* order, or — when the collection declares nothing — the EC-458 value-shape
|
|
209
|
+
* pick, unchanged. A declared field the document has no value for contributes
|
|
210
|
+
* nothing (an empty body is legitimate); a declared field holding something that
|
|
211
|
+
* is neither composition nor rich text contributes a visible fallback.
|
|
212
|
+
*/
|
|
213
|
+
declare function bodyItemsOf(document: Record<string, unknown> | null | undefined, presentation?: DeliveryPresentation): DocumentBodyItem[];
|
|
181
214
|
|
|
182
215
|
/**
|
|
183
216
|
* The content seam of the embedded runtime (EC-144, vision AD-1/AD-4): the
|
|
@@ -224,6 +257,16 @@ interface ContentSource {
|
|
|
224
257
|
* explicit routes.
|
|
225
258
|
*/
|
|
226
259
|
readonly routes?: readonly RouteRecord[];
|
|
260
|
+
/**
|
|
261
|
+
* The project's collection definitions (EC-499, additive) — config-owned
|
|
262
|
+
* (AD-11), already prefetched by every source that reads a schema. The route
|
|
263
|
+
* helper reads the resolved document's collection from here to honour its
|
|
264
|
+
* declared `presentation` (which field(s) hold the delivery body, and the
|
|
265
|
+
* optional detail wrapper). Optional so existing custom sources stay valid;
|
|
266
|
+
* without it every collection is treated as undeclared and the EC-458 shape
|
|
267
|
+
* picker decides, exactly as before.
|
|
268
|
+
*/
|
|
269
|
+
readonly collections?: readonly CollectionDef[];
|
|
227
270
|
/**
|
|
228
271
|
* Data-source definitions the graph's bindings reference (EC-166 delivery,
|
|
229
272
|
* additive). When present, the route helper materializes per-section list
|
|
@@ -306,13 +349,36 @@ interface MaterializedSourcePayloads {
|
|
|
306
349
|
*/
|
|
307
350
|
declare function materializeSourcePayloads(input: MaterializeSourcePayloadsInput): MaterializedSourcePayloads;
|
|
308
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Bridge from a `PersistenceAdapter` to the synchronous `ContentLookup` the
|
|
354
|
+
* accessors (EC-143) read through: prefetch the project's collections,
|
|
355
|
+
* documents, assets, and graphs ONCE (async), then serve every accessor call
|
|
356
|
+
* synchronously from memory. This is how `apps/example-site` reads the seeded
|
|
357
|
+
* in-memory fixture project; a Convex-backed `ContentSource` (EC-145/146)
|
|
358
|
+
* replaces this bridge without touching the route helper.
|
|
359
|
+
*
|
|
360
|
+
* The assembly half is exposed separately as {@link createStaticContentSource}
|
|
361
|
+
* (EC-156): a host that already holds the prefetched data as plain values —
|
|
362
|
+
* e.g. a delivery-snapshot payload fetched over Convex's public HTTP query
|
|
363
|
+
* API — assembles the exact same `ContentSource` without faking an adapter.
|
|
364
|
+
*/
|
|
309
365
|
interface ContentSnapshotOptions {
|
|
310
366
|
/** Route records served by `resolvePage` (EC-019). */
|
|
311
367
|
routes?: readonly RouteRecord[];
|
|
312
368
|
/** Redirect records served by `resolvePage` (EC-019/020). */
|
|
313
369
|
redirects?: readonly RedirectRecord[];
|
|
314
|
-
/**
|
|
315
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Locale configuration — **required**, never defaulted (EC-530). The default
|
|
372
|
+
* locale is the storage key every localized value is written under, so a
|
|
373
|
+
* fabricated `en` is a data migration to undo, not a config edit: content
|
|
374
|
+
* authored under one locale and served under another, silently (EC-512
|
|
375
|
+
* measured exactly that). EC-490 made a config declaring no locales an error
|
|
376
|
+
* and EC-519 collapsed the two declarations into one; this was the last place
|
|
377
|
+
* in the chain still inventing the value, and it sits in the package that
|
|
378
|
+
* resolves the URLs. Hand it the project's `LocaleConfig` — the one the
|
|
379
|
+
* config declares.
|
|
380
|
+
*/
|
|
381
|
+
locales: LocaleConfig;
|
|
316
382
|
/**
|
|
317
383
|
* Data-source definitions the graph's bindings reference (EC-166): section
|
|
318
384
|
* sources (`cms.section.<nodeId>`) carry their stored where/sort/limit, and
|
|
@@ -332,8 +398,8 @@ interface ContentSnapshot extends ContentSource {
|
|
|
332
398
|
*/
|
|
333
399
|
interface StaticContentData {
|
|
334
400
|
projectId: string;
|
|
335
|
-
/** Locale configuration
|
|
336
|
-
locales
|
|
401
|
+
/** Locale configuration — **required**, never defaulted (see EC-530 above). */
|
|
402
|
+
locales: LocaleConfig;
|
|
337
403
|
/** Route records served by `resolvePage` (EC-019). */
|
|
338
404
|
routes?: readonly RouteRecord[];
|
|
339
405
|
/** Redirect records served by `resolvePage` (EC-019/020). */
|
|
@@ -374,53 +440,7 @@ declare function createStaticContentSource(data: StaticContentData): ContentSnap
|
|
|
374
440
|
* request (or memoize across requests for fixture-backed sites) — accessors
|
|
375
441
|
* never reach back to the adapter.
|
|
376
442
|
*/
|
|
377
|
-
declare function createContentSnapshot(adapter: PersistenceAdapter, projectId: string, options
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* The locale ⇄ URL mapping, owned in ONE place (EC-486).
|
|
381
|
-
*
|
|
382
|
-
* The delivery host reads a request URL and derives `{ locale, path }`; the
|
|
383
|
-
* sitemap goes the other way, turning an enumerated `{ locale, path }` back
|
|
384
|
-
* into the URL a crawler will request. Those are inverse functions, and while
|
|
385
|
-
* they lived in two files they disagreed: a page whose top-level slug happens
|
|
386
|
-
* to equal a **non-default locale code** (`/de` in a project whose locales are
|
|
387
|
-
* `['en','de']`, default `en`) was published to `sitemap.xml` as
|
|
388
|
-
* `https://site/de`, which the host then split as *locale `de`, path `/`* and
|
|
389
|
-
* 404'd. Measured — see EC-486.
|
|
390
|
-
*
|
|
391
|
-
* Keeping both directions here makes the round-trip checkable, which is what
|
|
392
|
-
* {@link localeUrlPath} reports: some `{ locale, path }` pairs simply have **no
|
|
393
|
-
* URL that addresses them**, and a caller that publishes URLs must be able to
|
|
394
|
-
* tell rather than emit a link nothing serves.
|
|
395
|
-
*/
|
|
396
|
-
/**
|
|
397
|
-
* Derive the request locale from a leading URL segment (`/de/about` → locale
|
|
398
|
-
* `de`, path `/about`) and return the remaining path. No prefix (or the
|
|
399
|
-
* default locale's own content at root) falls back to the default locale.
|
|
400
|
-
*/
|
|
401
|
-
declare function splitLocalePath(url: string, locales: LocaleConfig): {
|
|
402
|
-
locale: Locale;
|
|
403
|
-
path: string;
|
|
404
|
-
};
|
|
405
|
-
interface LocaleUrlPath {
|
|
406
|
-
/** The request path a client uses to reach `path` in `locale`. */
|
|
407
|
-
urlPath: string;
|
|
408
|
-
/**
|
|
409
|
-
* Whether {@link splitLocalePath} recovers exactly the `{ locale, path }`
|
|
410
|
-
* this was built from. `false` means the pair is **unaddressable**: every URL
|
|
411
|
-
* that could denote it is read by the host as a different locale/path pair,
|
|
412
|
-
* so the content is unreachable and the URL must not be published.
|
|
413
|
-
*/
|
|
414
|
-
addressable: boolean;
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* The inverse of {@link splitLocalePath}: default locale serves the root,
|
|
418
|
-
* other locales get a `/<locale>` prefix — then the result is round-tripped
|
|
419
|
-
* back through `splitLocalePath` so an unaddressable pair is reported rather
|
|
420
|
-
* than silently emitted (AGENTS.md §4: never a fallback that hides a broken
|
|
421
|
-
* state).
|
|
422
|
-
*/
|
|
423
|
-
declare function localeUrlPath(path: string, locale: Locale, locales: LocaleConfig): LocaleUrlPath;
|
|
443
|
+
declare function createContentSnapshot(adapter: PersistenceAdapter, projectId: string, options: ContentSnapshotOptions): Promise<ContentSnapshot>;
|
|
424
444
|
|
|
425
445
|
/**
|
|
426
446
|
* The pure half of the catch-all route helper (EC-144): URL → outcome, with
|
|
@@ -429,6 +449,43 @@ declare function localeUrlPath(path: string, locale: Locale, locales: LocaleConf
|
|
|
429
449
|
* each outcome onto `redirect()` / `permanentRedirect()` / `notFound()` /
|
|
430
450
|
* `<CanvasRenderer />`.
|
|
431
451
|
*/
|
|
452
|
+
/** What the route helper tells a host callback about the request it is serving. */
|
|
453
|
+
interface CanvasRequestInfo {
|
|
454
|
+
/** The raw request path (including any locale prefix). */
|
|
455
|
+
url: string;
|
|
456
|
+
perspective: Perspective;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* What a host's `wrap` function is told about the request it is wrapping
|
|
460
|
+
* (EC-524 — see `CanvasRouteOptions.wrap`).
|
|
461
|
+
*
|
|
462
|
+
* Every field is something the route already had in hand at the wrap point:
|
|
463
|
+
* nothing is gathered for this seam, and nothing was invented for it.
|
|
464
|
+
*/
|
|
465
|
+
interface CanvasWrapContext extends CanvasRequestInfo {
|
|
466
|
+
/**
|
|
467
|
+
* The collection of the document this URL resolved to — **the branch key**.
|
|
468
|
+
* `undefined` when the resolution carried no document, so a host branching on
|
|
469
|
+
* a collection id naturally wraps nothing there (which is what the removed
|
|
470
|
+
* `presentation.wrapper` declaration did too: it only applied with a document).
|
|
471
|
+
*/
|
|
472
|
+
collection?: string;
|
|
473
|
+
/**
|
|
474
|
+
* The resolved document. Verbatim the `document` prop the removed wrapper
|
|
475
|
+
* component received, so a wrapper ported to this seam needs no other source
|
|
476
|
+
* for its field-level chrome (the title, a byline, a date).
|
|
477
|
+
*/
|
|
478
|
+
document: ResolvedDocument | null;
|
|
479
|
+
/**
|
|
480
|
+
* The whole resolution — the same value the `metadata` callback already
|
|
481
|
+
* receives, so this is the established host-callback argument on this route
|
|
482
|
+
* rather than a new surface. It carries `dynamicParams`, `route`,
|
|
483
|
+
* `localeFallback` and `issues` for a wrapper that needs more than the
|
|
484
|
+
* document (EC-499 recorded wanting exactly that and could not have it across
|
|
485
|
+
* a serializable declaration).
|
|
486
|
+
*/
|
|
487
|
+
result: ResolvePageResult;
|
|
488
|
+
}
|
|
432
489
|
/** What the host's payload factory sees for one request. */
|
|
433
490
|
interface CanvasPayloadContext {
|
|
434
491
|
/** Accessor client bound to this request's perspective + locale. */
|
|
@@ -500,6 +557,15 @@ type CanvasPageOutcome = {
|
|
|
500
557
|
* embedding page's href.
|
|
501
558
|
*/
|
|
502
559
|
links: Record<string, string>;
|
|
560
|
+
/**
|
|
561
|
+
* The resolved document's collection-declared delivery presentation
|
|
562
|
+
* (EC-499): which field(s) are the render body. Plain, serializable data —
|
|
563
|
+
* handed to `<CanvasRenderer />` so ONE catch-all renders every
|
|
564
|
+
* collection's body, whatever its shape. Absent when the collection
|
|
565
|
+
* declares nothing (or the source exposes no collections), and the EC-458
|
|
566
|
+
* value-shape picker then decides exactly as before.
|
|
567
|
+
*/
|
|
568
|
+
presentation?: DeliveryPresentation;
|
|
503
569
|
tags: CacheTags;
|
|
504
570
|
};
|
|
505
571
|
/** Build the request path from a Next catch-all `slug` param. */
|
|
@@ -525,28 +591,6 @@ interface ResolveCanvasPageOptions {
|
|
|
525
591
|
*/
|
|
526
592
|
declare function resolveCanvasPage(source: ContentSource, url: string, perspective: Perspective, options?: ResolveCanvasPageOptions): Promise<CanvasPageOutcome>;
|
|
527
593
|
|
|
528
|
-
/**
|
|
529
|
-
* Catch-all route helper (EC-144): the factory a host app calls once in
|
|
530
|
-
* `app/[[...slug]]/page.tsx` to wire `resolvePage` → `<CanvasRenderer />`,
|
|
531
|
-
* with redirects answered by Next's `redirect()`/`permanentRedirect()` (per
|
|
532
|
-
* the route's `permanent` flag), unknown URLs answered by `notFound()` (the
|
|
533
|
-
* host's 404 renders), and the perspective chosen by Next `draftMode()` —
|
|
534
|
-
* draft when enabled (via the token-gated preview route), published
|
|
535
|
-
* otherwise.
|
|
536
|
-
*
|
|
537
|
-
* With `cache` configured (EC-146), the published-perspective resolve+render
|
|
538
|
-
* data work runs inside `unstable_cache`, keyed by URL and tagged with the
|
|
539
|
-
* fetch-time tag set of what the response actually contained (see
|
|
540
|
-
* `cache.ts`). Publishing in the studio then invalidates exactly the affected
|
|
541
|
-
* entries via the signed `/api/revalidate` webhook — and while the content
|
|
542
|
-
* backend is unreachable, cached entries keep serving because a cache hit
|
|
543
|
-
* never invokes `loadContent`.
|
|
544
|
-
*/
|
|
545
|
-
interface CanvasRequestInfo {
|
|
546
|
-
/** The raw request path (including any locale prefix). */
|
|
547
|
-
url: string;
|
|
548
|
-
perspective: Perspective;
|
|
549
|
-
}
|
|
550
594
|
interface CanvasCacheOptions {
|
|
551
595
|
/**
|
|
552
596
|
* Project scope prefix for every tag (`p:<scope>:…`) — must be the project
|
|
@@ -600,6 +644,38 @@ interface CanvasRouteOptions {
|
|
|
600
644
|
onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
|
|
601
645
|
/** Override the generated `<head>` metadata for a resolved page. */
|
|
602
646
|
metadata?: (result: ResolvePageResult, info: CanvasRequestInfo) => Metadata;
|
|
647
|
+
/**
|
|
648
|
+
* Wrap the rendered page in the host's own chrome (EC-524) — a detail
|
|
649
|
+
* template, a per-collection layout, a draft ribbon. Receives the rendered
|
|
650
|
+
* canvas as `children` and returns whatever should stand in its place:
|
|
651
|
+
*
|
|
652
|
+
* ```tsx
|
|
653
|
+
* elytra.canvasRoute({
|
|
654
|
+
* wrap: (children, ctx) =>
|
|
655
|
+
* ctx.collection === 'post' ? <PostDetail document={ctx.document}>{children}</PostDetail> : children,
|
|
656
|
+
* })
|
|
657
|
+
* ```
|
|
658
|
+
*
|
|
659
|
+
* **This is deliberately host code, not config** (owner ruling 2026-08-01):
|
|
660
|
+
* *"this sounds very much like page-templates … templates belong in code, so
|
|
661
|
+
* the wrapper is something the developer has to care about … the page-content
|
|
662
|
+
* (Canvas or Rich-Text) should render agnostically."* A collection's
|
|
663
|
+
* `presentation.body` still names WHICH field is the content — that is content
|
|
664
|
+
* selection, and it is what lets one catch-all render a canvas page and a
|
|
665
|
+
* rich-text post through the same call. What surrounds that content is the
|
|
666
|
+
* developer's implementation decision, branched here in their own file.
|
|
667
|
+
*
|
|
668
|
+
* It replaced a `presentation.wrapper` component-id STRING resolved from the
|
|
669
|
+
* canvas registry, which forced a delivery-only component to register as a
|
|
670
|
+
* pseudo-block (EC-514). A function beats that on every axis that matters: it
|
|
671
|
+
* cannot dangle, the compiler checks it, and it can branch on anything in
|
|
672
|
+
* {@link CanvasWrapContext} rather than on collection identity alone.
|
|
673
|
+
*
|
|
674
|
+
* Applied to the `render` outcome only — a redirect or a 404 never reaches it.
|
|
675
|
+
* Returning `children` unchanged is the no-op, and omitting `wrap` entirely is
|
|
676
|
+
* the default.
|
|
677
|
+
*/
|
|
678
|
+
wrap?: (children: ReactNode, ctx: CanvasWrapContext) => ReactNode;
|
|
603
679
|
}
|
|
604
680
|
interface CanvasRouteProps {
|
|
605
681
|
params: Promise<{
|
|
@@ -750,14 +826,14 @@ declare function createCanvasSitemap(options: CreateCanvasSitemapOptions): () =>
|
|
|
750
826
|
* keeps multi-project hosts (one Next app serving several builder projects)
|
|
751
827
|
* from cross-invalidating, so the prefix is **on whenever the host knows its
|
|
752
828
|
* project id** — decision: include it. The emitter (the studio's webhook
|
|
753
|
-
* dispatcher, `
|
|
829
|
+
* dispatcher, `packages/studio/src/lib/webhooks`) always scopes with the
|
|
754
830
|
* operation's `projectId`; a host that omits `cache.scope` therefore will not
|
|
755
831
|
* match studio-emitted tags. Configure `cache.scope` with the same project id
|
|
756
832
|
* the studio publishes under.
|
|
757
833
|
*
|
|
758
834
|
* The studio-side emitter cannot depend on this package (it would drag the
|
|
759
|
-
* `next` peer dependency into the
|
|
760
|
-
* `
|
|
835
|
+
* `next` peer dependency into the studio bundle), so
|
|
836
|
+
* `packages/studio/src/lib/webhooks/tags.ts` mirrors these formatters; tests on
|
|
761
837
|
* both sides pin the exact literal strings to keep them in lock-step.
|
|
762
838
|
*/
|
|
763
839
|
/** Apply the optional multi-project scope prefix to one tag. */
|
|
@@ -832,14 +908,67 @@ interface CachedEntry<T> {
|
|
|
832
908
|
* entry and the next request repeats the cycle with fresh data.
|
|
833
909
|
*
|
|
834
910
|
* The loader runs **once per revalidation cycle** (per key), never once per
|
|
835
|
-
* request. Time-based revalidation is intentionally not exposed
|
|
836
|
-
* stale-while-revalidate background
|
|
837
|
-
*
|
|
838
|
-
* `revalidate: false`
|
|
911
|
+
* request. Time-based revalidation is intentionally not exposed, and the reason
|
|
912
|
+
* is a real hazard rather than a preference: a stale-while-revalidate background
|
|
913
|
+
* refresh re-runs the probe callback, whose bail aborts the refresh, so the
|
|
914
|
+
* entry is never rewritten. `revalidate: false` keeps *time* from ever putting
|
|
915
|
+
* an entry into that state — which is the AD-5 model.
|
|
916
|
+
*
|
|
917
|
+
* **What `revalidate: false` does NOT buy (EC-515, measured on Next 16.2.10).**
|
|
918
|
+
* The earlier version of this comment claimed `revalidate: false` made the SWR
|
|
919
|
+
* hazard unreachable. It does not: a **tag** can put an entry into the stale
|
|
920
|
+
* state too, and then exactly the aborted-refresh loop above happens. Whether it
|
|
921
|
+
* does is decided by how the invalidator stamps the tag —
|
|
922
|
+
* `revalidateTag(tag, 'max')` marks entries stale (365-day expiry) and triggers
|
|
923
|
+
* the loop; `revalidateTag(tag, { expire: 0 })` drops them outright and does
|
|
924
|
+
* not. `revalidate.ts` therefore sends `{ expire: 0 }`, and the reasoning lives
|
|
925
|
+
* at that call site. Keep the two in step: this design only survives an
|
|
926
|
+
* invalidation that **drops** the entry, never one that merely staleness-marks
|
|
927
|
+
* it.
|
|
839
928
|
*
|
|
840
929
|
* Concurrency: wrappers and the captured closure are per-request, so there is
|
|
841
930
|
* no cross-request mutation; two racing first requests both resolve and the
|
|
842
931
|
* last write wins (deterministic content ⇒ identical entries).
|
|
932
|
+
*
|
|
933
|
+
* ## ⚠️ Self-hosted hosts: only the BASELINE tags invalidate (EC-515)
|
|
934
|
+
*
|
|
935
|
+
* **Measured on a self-hosted `next start` (Next 16.2.10, default
|
|
936
|
+
* `FileSystemCache`, `.next/cache` on disk); NOT measured on Vercel.**
|
|
937
|
+
*
|
|
938
|
+
* Fetch-time tag discovery writes the entry with the tags the response actually
|
|
939
|
+
* read — but the default self-hosted cache handler decides staleness and expiry
|
|
940
|
+
* from the tags the **reading** wrapper names (`ctx.tags`, see
|
|
941
|
+
* `IncrementalCache.get` / `FileSystemCache.get` for `kind: FETCH`), not from
|
|
942
|
+
* the tags the entry was **stored** with. The stored tag set is only ever used
|
|
943
|
+
* to decide whether to re-write the entry with a superset.
|
|
944
|
+
*
|
|
945
|
+
* Every read in {@link loadCachedEntry} goes through the **probe** wrapper,
|
|
946
|
+
* whose tags are `baselineTags`. So on such a host:
|
|
947
|
+
*
|
|
948
|
+
* - revalidating a tag that is in `baselineTags` works (the route helper passes
|
|
949
|
+
* the project sweep tag, so `project` sweeps do invalidate);
|
|
950
|
+
* - revalidating a *discovered* `doc:` / `collection:` / `route:` / `asset:` tag
|
|
951
|
+
* invalidates **nothing** — silently. No error, no log line, no loader run.
|
|
952
|
+
*
|
|
953
|
+
* Measured 2×2 that isolates it (same host, same run): a probe handed the full
|
|
954
|
+
* discovered tag set up front DOES honour a `doc:` tag, and the same probe
|
|
955
|
+
* handed only the baseline does not. So the tags, their formatting and the
|
|
956
|
+
* webhook are all correct — the read simply never consults them.
|
|
957
|
+
*
|
|
958
|
+
* Consequence for a self-hosted deployment: invalidation granularity collapses
|
|
959
|
+
* to whatever is in `baselineTags`. The studio's Convex write chokepoint emits
|
|
960
|
+
* the project sweep on every publish, so instant publish still *works* there,
|
|
961
|
+
* coarsely; the browser dispatcher's targeted `doc:`/`collection:` tag set
|
|
962
|
+
* (`packages/studio/src/lib/webhooks/tags.ts`) does not reach a self-hosted host
|
|
963
|
+
* at all. Vercel-hosted deployments are unaffected in practice — both consumer
|
|
964
|
+
* sites were measured healthy on 2026-08-01 — which is consistent with Vercel's
|
|
965
|
+
* cache handler purging by the entry's stored tags, though that has **not** been
|
|
966
|
+
* measured here.
|
|
967
|
+
*
|
|
968
|
+
* Fixing this properly means the read must be able to name the entry's own tags,
|
|
969
|
+
* which no `unstable_cache` API allows without restructuring the probe/bail
|
|
970
|
+
* seam. That is a deliberate open decision — see EC-515, do not "fix" it here
|
|
971
|
+
* incidentally.
|
|
843
972
|
*/
|
|
844
973
|
declare function loadCachedEntry<T>(cache: CacheWrapper, keyParts: string[], baselineTags: readonly string[], load: () => Promise<CachedEntry<T>>): Promise<CachedEntry<T>>;
|
|
845
974
|
|
|
@@ -1298,16 +1427,13 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
|
|
|
1298
1427
|
* ```ts
|
|
1299
1428
|
* // frontend/lib/elytra.ts — the ONE seam file
|
|
1300
1429
|
* import { createElytraHost } from '@elytracms/next'
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1303
|
-
*
|
|
1304
|
-
*
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
* routes,
|
|
1309
|
-
* components: siteComponents,
|
|
1310
|
-
* })
|
|
1430
|
+
* // Read the project out of the repo's own config — `elytra.config.ts` is the
|
|
1431
|
+
* // single contract surface, and importing it costs a delivery bundle nothing
|
|
1432
|
+
* // (EC-532). Any other module layout works just as well; this is an example,
|
|
1433
|
+
* // not a required path.
|
|
1434
|
+
* import { site } from '../../elytra.config'
|
|
1435
|
+
*
|
|
1436
|
+
* export const elytra = createElytraHost({ ...site })
|
|
1311
1437
|
* ```
|
|
1312
1438
|
*
|
|
1313
1439
|
* ```ts
|
|
@@ -1339,7 +1465,23 @@ interface ElytraHostOptions {
|
|
|
1339
1465
|
* Used as the content scope when no live backend is configured.
|
|
1340
1466
|
*/
|
|
1341
1467
|
slug: string;
|
|
1342
|
-
/**
|
|
1468
|
+
/**
|
|
1469
|
+
* Locale config — config-owned (AD-11), never read from the snapshot.
|
|
1470
|
+
*
|
|
1471
|
+
* **Import it, do not restate it** (EC-519). This is the *same*
|
|
1472
|
+
* {@link LocaleConfig} `elytra.config.ts` takes, so a project declares its
|
|
1473
|
+
* locales once and hands that one binding to both mounts. It stays a host
|
|
1474
|
+
* option because the engine cannot know where a repo keeps the value — but it
|
|
1475
|
+
* is no longer an option you have to *retype*: since EC-532 the delivery
|
|
1476
|
+
* bundle can import `elytra.config.ts` itself for zero engine modules
|
|
1477
|
+
* (measured), so `createElytraHost({ ...site })` over a project exported from
|
|
1478
|
+
* the config is the shortest correct answer.
|
|
1479
|
+
*
|
|
1480
|
+
* This used to be `{ default, locales }` against a config spelling the same
|
|
1481
|
+
* fact `locales` + `defaultLocale`, and in our own demo the two drifted — the
|
|
1482
|
+
* host said `['en']`, the config said `['en','de']`, and every `/de` URL 404'd
|
|
1483
|
+
* silently for weeks (EC-512).
|
|
1484
|
+
*/
|
|
1343
1485
|
locales: LocaleConfig;
|
|
1344
1486
|
/** CMS collection schema — config-owned (AD-11). */
|
|
1345
1487
|
collections: readonly CollectionDef[];
|
|
@@ -1350,8 +1492,26 @@ interface ElytraHostOptions {
|
|
|
1350
1492
|
* SAME modules the studio composes with). Base primitives are always
|
|
1351
1493
|
* included; `base.Image` is swapped for `next/image` delivery by default
|
|
1352
1494
|
* (see `image`).
|
|
1495
|
+
*
|
|
1496
|
+
* Typed as the CONFIG's `HostComponentEntry` (`implementation: unknown`), not
|
|
1497
|
+
* the stricter {@link HostComponent}, so that `createElytraHost({ ...site })`
|
|
1498
|
+
* — the shape EC-532 makes the contract — compiles however the consumer typed
|
|
1499
|
+
* their project entry. It previously demanded `ComponentType`, which meant the
|
|
1500
|
+
* recommended spread worked only when the entry's implementation types
|
|
1501
|
+
* survived inference: `apps/demo/elytra.config.ts` gets away with it via
|
|
1502
|
+
* `satisfies`, and anyone using the engine's own published `HostComponentEntry`
|
|
1503
|
+
* / `WorkspaceProjectEntry` annotation did not. Both production consumers hit
|
|
1504
|
+
* exactly that and each carried one hand-restated `components` line (EC-539).
|
|
1505
|
+
*
|
|
1506
|
+
* What this costs, stated plainly rather than glossed: a list passed HERE no
|
|
1507
|
+
* longer gets a compile-time check that each `implementation` is really a
|
|
1508
|
+
* component, and nothing yet checks it at runtime either — a garbage value
|
|
1509
|
+
* would reach the renderer. `defineHostComponents` keeps the strict
|
|
1510
|
+
* {@link HostComponent} signature, so a hand-built list passed there directly
|
|
1511
|
+
* is still checked. Closing the gap properly needs a registration issue code
|
|
1512
|
+
* in the core registry; booked as EC-541 rather than smuggled in here.
|
|
1353
1513
|
*/
|
|
1354
|
-
components?: readonly
|
|
1514
|
+
components?: readonly HostComponentEntry[];
|
|
1355
1515
|
/**
|
|
1356
1516
|
* `base.Image` delivery options (loader/sizes/quality — the non-Vercel CDN
|
|
1357
1517
|
* seam), or `false` to keep the plain-`<img>` primitive. Default: Next's own
|
|
@@ -1390,6 +1550,12 @@ interface HostCanvasRouteOptions {
|
|
|
1390
1550
|
onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
|
|
1391
1551
|
/** Metadata override (see `CanvasRouteOptions.metadata`). */
|
|
1392
1552
|
metadata?: (result: ResolvePageResult, info: CanvasRequestInfo) => Metadata;
|
|
1553
|
+
/**
|
|
1554
|
+
* Host-owned chrome around the rendered page (EC-524) — the seam a per-
|
|
1555
|
+
* collection detail template lives on. Branch on `ctx.collection` in your own
|
|
1556
|
+
* route file (see `CanvasRouteOptions.wrap`).
|
|
1557
|
+
*/
|
|
1558
|
+
wrap?: (children: ReactNode, ctx: CanvasWrapContext) => ReactNode;
|
|
1393
1559
|
/**
|
|
1394
1560
|
* Tagged data cache (instant publish). Default: enabled, scoped to the
|
|
1395
1561
|
* kit's content scope so the studio's webhook tags match. `false` disables
|
|
@@ -1401,6 +1567,25 @@ interface LoadGlobalsOptions {
|
|
|
1401
1567
|
/** Perspective of the chrome read. Default `'published'` (live is published-only). */
|
|
1402
1568
|
perspective?: Perspective;
|
|
1403
1569
|
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Which world {@link ElytraHost.loadContent} reads from.
|
|
1572
|
+
*
|
|
1573
|
+
* - `live` — the configured Convex deployment (published; token-gated draft).
|
|
1574
|
+
* - `fallback` — the host's own `fallbackContent` world (the scaffold's seeded
|
|
1575
|
+
* starter, the demo's in-memory fixture). A legitimate, supported mode.
|
|
1576
|
+
* - `empty` — no backend and no fallback: this project's structure with no
|
|
1577
|
+
* content, so every URL is a clean 404. Correct for a CI build; almost never
|
|
1578
|
+
* what a deployed site intends.
|
|
1579
|
+
*
|
|
1580
|
+
* The mode was decided from env long before EC-527 and readable by **nobody**,
|
|
1581
|
+
* so a site serving the fallback looked identical from the outside to one
|
|
1582
|
+
* reading the real deployment. Measured on the demo: `/` returned `200` while
|
|
1583
|
+
* all four of EC-512's published fixtures 404'd — an agent following AGENTS.md
|
|
1584
|
+
* §5 gets no signal that the thing under test was never exercised. Declared
|
|
1585
|
+
* once, read here, so a host can *say* which world it is in instead of
|
|
1586
|
+
* re-deriving it from `env` and drifting.
|
|
1587
|
+
*/
|
|
1588
|
+
type ElytraContentMode = 'live' | 'fallback' | 'empty';
|
|
1404
1589
|
/** The delivery host handle {@link createElytraHost} yields. */
|
|
1405
1590
|
interface ElytraHost {
|
|
1406
1591
|
/**
|
|
@@ -1408,6 +1593,13 @@ interface ElytraHost {
|
|
|
1408
1593
|
* studio's webhook tags), else the config slug.
|
|
1409
1594
|
*/
|
|
1410
1595
|
contentScope: string;
|
|
1596
|
+
/**
|
|
1597
|
+
* Which world {@link loadContent} reads from — see {@link ElytraContentMode}.
|
|
1598
|
+
* Read it to announce the mode (a boot line, a dev-only banner) rather than
|
|
1599
|
+
* re-deriving it from {@link env}, which is the drift this field exists to
|
|
1600
|
+
* prevent.
|
|
1601
|
+
*/
|
|
1602
|
+
contentMode: ElytraContentMode;
|
|
1411
1603
|
/** The resolved env bag (documented names, trimmed, empty = unset). */
|
|
1412
1604
|
env: ElytraHostEnv;
|
|
1413
1605
|
/** Materialized route records (a derive is run once, at factory time). */
|
|
@@ -1533,6 +1725,12 @@ declare function loadLiveContent(options: LiveContentOptions, perspective: Persp
|
|
|
1533
1725
|
* document, empty composition, unknown component, or an undeclared hint all
|
|
1534
1726
|
* answer `false` — the safe default is clearance.
|
|
1535
1727
|
*
|
|
1728
|
+
* EC-499: pass the collection's resolved `presentation` (from the render
|
|
1729
|
+
* outcome) so this reads the SAME body the renderer will render when the
|
|
1730
|
+
* collection declares one — otherwise the value-shape pick could name a
|
|
1731
|
+
* different field than the declaration does. A body whose first item is prose
|
|
1732
|
+
* rather than a block answers `false`, which is right: rich text opens no hero.
|
|
1733
|
+
*
|
|
1536
1734
|
* ```tsx
|
|
1537
1735
|
* const outcome = client.resolvePage(path)
|
|
1538
1736
|
* const fullBleed = firstBlockFullBleed(outcome.document, components.registry)
|
|
@@ -1541,7 +1739,7 @@ declare function loadLiveContent(options: LiveContentOptions, perspective: Persp
|
|
|
1541
1739
|
*
|
|
1542
1740
|
* The host kit wires the resolution half for you: `elytra.pageOpensFullBleed(slug)`.
|
|
1543
1741
|
*/
|
|
1544
|
-
declare function firstBlockFullBleed(document: Record<string, unknown> | null | undefined, registry: ComponentRegistry): boolean;
|
|
1742
|
+
declare function firstBlockFullBleed(document: Record<string, unknown> | null | undefined, registry: ComponentRegistry, presentation?: DeliveryPresentation): boolean;
|
|
1545
1743
|
|
|
1546
1744
|
/**
|
|
1547
1745
|
* Pure token/target logic of the draft-preview routes (EC-144), kept free of
|
|
@@ -1589,4 +1787,4 @@ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken:
|
|
|
1589
1787
|
*/
|
|
1590
1788
|
declare const PACKAGE = "@elytracms/next";
|
|
1591
1789
|
|
|
1592
|
-
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
|
|
1790
|
+
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 CanvasWrapContext, type ContentSnapshot, type ContentSnapshotOptions, type ContentSource, type CreateCanvasSitemapOptions, type DefineHostComponentsOptions, type DeliveryPresentation, ELYTRA_HOST_ENV_KEYS, type ElytraContentMode, 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, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, isSourcePayloadError, loadCachedEntry, loadLiveContent, materializeSourcePayloads, mergeCacheTags, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError };
|