@crombolo/next-integration 0.0.0-stage → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kineco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,69 @@
1
+ crombolo
2
+ Copyright (c) 2026 Kineco
3
+
4
+ This product includes third-party software. crombolo is an independent project.
5
+ It is not affiliated with, endorsed by, or sponsored by Umbraco A/S, which owns
6
+ the Umbraco trademark.
7
+
8
+ There are three places third-party material is redistributed, rather than merely
9
+ depended on:
10
+
11
+ @crombolo/backoffice-dist the built Umbraco backoffice and the browser
12
+ packages bundled into it, produced by
13
+ `bun run vendor:backoffice`
14
+ @crombolo/backoffice-dist three CSS files copied unmodified from the Umbraco
15
+ client source tree
16
+ @crombolo/contracts the Umbraco Management API OpenAPI document,
17
+ copied unmodified
18
+
19
+ No Umbraco trademark is redistributed. The Umbraco wordmark, the "U" roundel
20
+ favicon and the installer illustration are deliberately not carried in this
21
+ repository: MIT grants copyright permission and says nothing about trademarks, so
22
+ crombolo's own marks are served at those paths instead. See `BRANDED_ASSETS` in
23
+ `@crombolo/backoffice-host`.
24
+
25
+ The Umbraco backoffice
26
+ ----------------------
27
+ @umbraco-cms/backoffice 18.2.0 — MIT — https://github.com/umbraco/Umbraco-CMS
28
+ @umbraco-ui/uui 2.0.2 — MIT — https://github.com/umbraco/Umbraco.UI
29
+
30
+ Static files from the Umbraco client source tree
31
+ ------------------------------------------------
32
+ css/umb-css.css, css/rte-content.css, css/umbraco-blockgridlayout.css
33
+ — copied unmodified from Umbraco-CMS release-18.2.0 — MIT
34
+ — https://github.com/umbraco/Umbraco-CMS
35
+ Shipped in @crombolo/backoffice-dist under `upstream-static/`.
36
+
37
+ `rte-content.css` itself carries Tiptap's default editor styles, copied from
38
+ tiptap v2.11.7 (MIT); the file records this in its own header.
39
+
40
+ The Umbraco Management API contract
41
+ -----------------------------------
42
+ OpenApi.json — copied unmodified from Umbraco-CMS release-18.2.0
43
+ (src/Umbraco.Cms.Api.Management/OpenApi.json) — MIT
44
+ — https://github.com/umbraco/Umbraco-CMS
45
+ Shipped in @crombolo/contracts, which also ships the TypeScript types generated
46
+ from it.
47
+
48
+ Browser runtime dependencies bundled into the backoffice build
49
+ --------------------------------------------------------------
50
+ @heximal/expressions 0.1.5 — BSD-3-Clause
51
+ @microsoft/signalr 10.0.11 — MIT
52
+ @tiptap/* 3.31.3 — MIT
53
+ diff 9.0.0 — BSD-3-Clause
54
+ dompurify 3.4.16 — Apache-2.0, elected from "MPL-2.0 OR Apache-2.0"
55
+ element-internals-polyfill 3.0.2 — MIT
56
+ lit 3.3.3 — BSD-3-Clause
57
+ luxon 3.7.2 — MIT
58
+ marked 18.0.14 — MIT
59
+ monaco-editor 0.57.0 — MIT
60
+ rxjs 7.8.2 — Apache-2.0
61
+ uuid 14.0.2 — MIT
62
+
63
+ dompurify is offered under either licence; Apache-2.0 is elected here, so no
64
+ MPL-2.0 obligation attaches to the copy bundled into the backoffice build. Of the
65
+ Apache-2.0 packages above, none ships a NOTICE file of its own, so Apache-2.0
66
+ section 4(d) adds nothing to this file.
67
+
68
+ Each package's own licence text ships inside its npm distribution and applies
69
+ unchanged to the copies bundled here.
@@ -0,0 +1,2 @@
1
+ import { type CacheAdapter } from './cache.ts';
2
+ export declare const cache15: CacheAdapter;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Next.js 15's cache for `createCrombolo` (story 208). Next 15 tags a fetch
3
+ * before it runs, so the keys an answer holds cannot tag it: every answer is
4
+ * kept under the site's tag, and any change clears them all.
5
+ */
6
+ import { revalidateTag } from 'next/cache';
7
+ import { keptHeaders, SITE_TAG } from './cache.js';
8
+ export const cache15 = {
9
+ major: 15,
10
+ async answer(url, headers) {
11
+ const response = await fetch(url, {
12
+ headers,
13
+ next: { tags: [SITE_TAG], revalidate: false },
14
+ });
15
+ const body = await response.text();
16
+ return { status: response.status, body, headers: keptHeaders(response.headers) };
17
+ },
18
+ revalidate() {
19
+ ;
20
+ revalidateTag(SITE_TAG);
21
+ },
22
+ };
@@ -0,0 +1,3 @@
1
+ import { type CacheAdapter, type KeptAnswer } from './cache.ts';
2
+ export declare function keptAnswer(url: string, headers: Record<string, string>): Promise<KeptAnswer>;
3
+ export declare const cache16: CacheAdapter;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Next.js 16's cache for `createCrombolo` (story 208): `'use cache'`, tagged
3
+ * from each answer with `cacheTag`, so a publish clears exactly what holds the
4
+ * page and its ancestors. Needs `cacheComponents: true` in `next.config.ts`.
5
+ */
6
+ import { cacheLife, cacheTag, revalidateTag } from 'next/cache';
7
+ import { keptHeaders, MISSING_TAG, tagOf, tagsFor, unkept, } from './cache.js';
8
+ export async function keptAnswer(url, headers) {
9
+ 'use cache';
10
+ const response = await fetch(url, { headers });
11
+ const body = await response.text();
12
+ // Kept for as long as nothing changes; the webhook says when something does.
13
+ if (unkept(response.headers))
14
+ cacheLife('seconds');
15
+ else
16
+ cacheLife('max');
17
+ cacheTag(...tagsFor(response.status, body, response.headers));
18
+ return { status: response.status, body, headers: keptHeaders(response.headers) };
19
+ }
20
+ export const cache16 = {
21
+ major: 16,
22
+ answer: keptAnswer,
23
+ revalidate(keys) {
24
+ for (const tag of [MISSING_TAG, ...keys.map(tagOf)])
25
+ revalidateTag(tag, 'max');
26
+ },
27
+ };
@@ -0,0 +1,23 @@
1
+ export interface KeptAnswer {
2
+ status: number;
3
+ body: string;
4
+ headers: Record<string, string>;
5
+ }
6
+ export interface CacheAdapter {
7
+ readonly major: 15 | 16;
8
+ /** The answer at `url`, from Next's cache when it holds one. */
9
+ answer(url: string, headers: Record<string, string>): Promise<KeptAnswer>;
10
+ /** Clears every kept answer holding one of these keys, and every kept not-found. */
11
+ revalidate(keys: readonly string[]): void;
12
+ }
13
+ /** Every adapter's answers carry this, so a whole site can be cleared at once. */
14
+ export declare const SITE_TAG = "crombolo";
15
+ /** Kept not-found answers, cleared on every change: a new page may answer one. */
16
+ export declare const MISSING_TAG = "crombolo:missing";
17
+ export declare const tagOf: (key: string) => string;
18
+ /** The tags an answer is kept under: the site's, and each key it holds or the site named. */
19
+ export declare function tagsFor(status: number, body: string, headers: Headers): string[];
20
+ /** Whether the site said not to keep it: a redirect, a form, anything personal. */
21
+ export declare function unkept(headers: Headers): boolean;
22
+ /** The headers a kept answer needs to be read again. */
23
+ export declare function keptHeaders(headers: Headers): Record<string, string>;
package/dist/cache.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * What a caching adapter does for `createCrombolo`, one per Next.js major
3
+ * (story 208): keep the Delivery API's answers in Next's cache, tagged with the
4
+ * content they hold, and clear them when the site says it changed.
5
+ */
6
+ import { contentKeysIn } from '@crombolo/delivery-client';
7
+ /** Every adapter's answers carry this, so a whole site can be cleared at once. */
8
+ export const SITE_TAG = 'crombolo';
9
+ /** Kept not-found answers, cleared on every change: a new page may answer one. */
10
+ export const MISSING_TAG = 'crombolo:missing';
11
+ /** Next.js keeps at most this many tags on one entry. */
12
+ const MAX_TAGS = 128;
13
+ export const tagOf = (key) => `crombolo:${key.toLowerCase()}`;
14
+ /** The tags an answer is kept under: the site's, and each key it holds or the site named. */
15
+ export function tagsFor(status, body, headers) {
16
+ if (status === 404)
17
+ return [SITE_TAG, MISSING_TAG];
18
+ let parsed = null;
19
+ try {
20
+ parsed = body ? JSON.parse(body) : null;
21
+ }
22
+ catch { }
23
+ const named = (headers.get('surrogate-key') ?? headers.get('cache-tag') ?? '')
24
+ .split(/[\s,]+/)
25
+ .filter(Boolean);
26
+ const keys = new Set([...contentKeysIn(parsed), ...named.map((k) => k.toLowerCase())]);
27
+ return [SITE_TAG, ...[...keys].map(tagOf)].slice(0, MAX_TAGS);
28
+ }
29
+ /** Whether the site said not to keep it: a redirect, a form, anything personal. */
30
+ export function unkept(headers) {
31
+ return /\b(no-store|private)\b/i.test(headers.get('cache-control') ?? '');
32
+ }
33
+ /** The headers a kept answer needs to be read again. */
34
+ export function keptHeaders(headers) {
35
+ const out = {};
36
+ for (const name of ['content-type', 'etag', 'cache-control', 'surrogate-key', 'cache-tag']) {
37
+ const value = headers.get(name);
38
+ if (value !== null)
39
+ out[name] = value;
40
+ }
41
+ return out;
42
+ }
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function RichTextLinks({ children, className, }: {
3
+ children: ReactNode;
4
+ className?: string;
5
+ }): import("react").JSX.Element;
package/dist/client.js ADDED
@@ -0,0 +1,26 @@
1
+ /** @jsxImportSource react */
2
+ 'use client';
3
+ import { jsx as _jsx } from "react/jsx-runtime";
4
+ /**
5
+ * The browser half of `<RichText>` (story 208): a click on a link to a page of
6
+ * this site navigates with Next's router rather than reloading the page.
7
+ */
8
+ import { useRouter } from 'next/navigation';
9
+ export function RichTextLinks({ children, className, }) {
10
+ const router = useRouter();
11
+ const onClick = (event) => {
12
+ if (event.defaultPrevented || event.button !== 0)
13
+ return;
14
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
15
+ return;
16
+ const link = event.target.closest('a');
17
+ const href = link?.getAttribute('href');
18
+ if (!link || !href || link.target || link.hasAttribute('download'))
19
+ return;
20
+ if (!href.startsWith('/') || href.startsWith('//') || href.startsWith('/media/'))
21
+ return;
22
+ event.preventDefault();
23
+ router.push(href);
24
+ };
25
+ return (_jsx("div", { className: className, onClick: onClick, children: children }));
26
+ }
@@ -0,0 +1,31 @@
1
+ /** @jsxImportSource react */
2
+ /**
3
+ * Server components for crombolo values (story 208): rich text, and block lists
4
+ * and grids rendered through a component per element type.
5
+ */
6
+ import type { ApiBlockGrid, ApiBlockList, ApiElement, ApiRichText } from '@crombolo/delivery-client';
7
+ import type { ComponentType } from 'react';
8
+ /**
9
+ * Rich text as the CMS wrote it, with links to its pages navigating in the app.
10
+ * The markup is the CMS's own, already sanitised there.
11
+ */
12
+ export declare function RichText({ value, className, }: {
13
+ value: ApiRichText | null | undefined;
14
+ className?: string;
15
+ }): import("react").JSX.Element | null;
16
+ /** What a block's component is given. */
17
+ export interface BlockComponentProps<Content = ApiElement, Settings = ApiElement> {
18
+ content: Content;
19
+ settings: Settings | null;
20
+ }
21
+ export type BlockComponents = Record<string, ComponentType<BlockComponentProps<never, never>>>;
22
+ /** A block list, each item through the component for its element type. */
23
+ export declare function BlockList({ value, components, }: {
24
+ value: ApiBlockList | null | undefined;
25
+ components: BlockComponents;
26
+ }): import("react").JSX.Element | null;
27
+ /** A block grid, laid out in its columns, areas included. */
28
+ export declare function BlockGrid({ value, components, }: {
29
+ value: ApiBlockGrid | null | undefined;
30
+ components: BlockComponents;
31
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,45 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { draftMode } from 'next/headers';
3
+ import { RichTextLinks } from './client.js';
4
+ /**
5
+ * Rich text as the CMS wrote it, with links to its pages navigating in the app.
6
+ * The markup is the CMS's own, already sanitised there.
7
+ */
8
+ export function RichText({ value, className, }) {
9
+ if (!value?.markup)
10
+ return null;
11
+ return (_jsx(RichTextLinks, { className: className, children: _jsx("div", { dangerouslySetInnerHTML: { __html: value.markup } }) }));
12
+ }
13
+ async function Missing({ alias }) {
14
+ if (!(await draftMode()).isEnabled)
15
+ return null;
16
+ return (_jsxs("div", { "data-crombolo-missing-block": alias, style: { border: '1px dashed #c00', padding: '1rem' }, children: ["There is no component for the block \u201C", alias, "\u201D yet."] }));
17
+ }
18
+ function Block({ content, settings, components, }) {
19
+ if (!content)
20
+ return null;
21
+ const Component = components[content.contentType];
22
+ return Component ? (_jsx(Component, { content: content, settings: settings })) : (_jsx(Missing, { alias: content.contentType }));
23
+ }
24
+ /** A block list, each item through the component for its element type. */
25
+ export function BlockList({ value, components, }) {
26
+ if (!value?.items.length)
27
+ return null;
28
+ return (_jsx(_Fragment, { children: value.items.map((item, index) => (_jsx(Block, { content: item.content, settings: item.settings, components: components }, item.content?.id ?? index))) }));
29
+ }
30
+ function GridItems({ items, components, }) {
31
+ return (_jsx(_Fragment, { children: items.map((item, index) => (_jsxs("div", { className: "crombolo-grid-item", style: { gridColumn: `span ${item.columnSpan}`, gridRow: `span ${item.rowSpan}` }, children: [_jsx(Block, { content: item.content, settings: item.settings, components: components }), item.areas.map((area) => (_jsx("div", { className: "crombolo-grid-area", "data-area": area.alias, style: {
32
+ display: 'grid',
33
+ gridTemplateColumns: `repeat(${item.areaGridColumns}, minmax(0, 1fr))`,
34
+ gridColumn: `span ${area.columnSpan}`,
35
+ }, children: _jsx(GridItems, { items: area.items, components: components }) }, area.alias)))] }, item.content?.id ?? index))) }));
36
+ }
37
+ /** A block grid, laid out in its columns, areas included. */
38
+ export function BlockGrid({ value, components, }) {
39
+ if (!value?.items.length)
40
+ return null;
41
+ return (_jsx("div", { className: "crombolo-grid", style: {
42
+ display: 'grid',
43
+ gridTemplateColumns: `repeat(${value.gridColumns}, minmax(0, 1fr))`,
44
+ }, children: _jsx(GridItems, { items: value.items, components: components }) }));
45
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `next/image` for crombolo media (story 208): the CMS resizes, so the loader
3
+ * writes its imaging query rather than Next's optimiser doing the work.
4
+ *
5
+ * ```ts
6
+ * // next.config.ts
7
+ * images: { loader: 'custom', loaderFile: './lib/image-loader.ts' }
8
+ * // lib/image-loader.ts
9
+ * export { default } from '@crombolo/next-integration/image-loader'
10
+ * ```
11
+ */
12
+ import type { ApiCrop, ApiFocalPoint } from '@crombolo/delivery-client';
13
+ export interface ImageLoaderProps {
14
+ src: string;
15
+ width: number;
16
+ quality?: number;
17
+ }
18
+ /** Adds the width, quality and a modern format to a media address, keeping a crop it carries. */
19
+ export default function cromboloImageLoader({ src, width, quality }: ImageLoaderProps): string;
20
+ /** What a picked image needs for `<Image>`: an address carrying its crop or focal point, and its size. */
21
+ export declare function cromboloImage(media: {
22
+ url: string;
23
+ width: number | null;
24
+ height: number | null;
25
+ name: string;
26
+ focalPoint?: ApiFocalPoint | null;
27
+ crops?: ApiCrop[] | null;
28
+ }, options?: {
29
+ crop?: string;
30
+ alt?: string;
31
+ }): {
32
+ src: string;
33
+ width: number;
34
+ height: number;
35
+ alt: string;
36
+ };
@@ -0,0 +1,37 @@
1
+ /** Adds the width, quality and a modern format to a media address, keeping a crop it carries. */
2
+ export default function cromboloImageLoader({ src, width, quality }) {
3
+ const [path, query = ''] = src.split('?');
4
+ const params = new URLSearchParams(query);
5
+ params.set('width', String(width));
6
+ if (!params.has('quality'))
7
+ params.set('quality', String(quality ?? 75));
8
+ if (!params.has('format'))
9
+ params.set('format', 'webp');
10
+ return `${path}?${params.toString()}`;
11
+ }
12
+ /** What a picked image needs for `<Image>`: an address carrying its crop or focal point, and its size. */
13
+ export function cromboloImage(media, options = {}) {
14
+ const params = new URLSearchParams();
15
+ const crop = options.crop ? media.crops?.find((c) => c.alias === options.crop) : undefined;
16
+ let width = media.width ?? 0;
17
+ let height = media.height ?? 0;
18
+ if (crop) {
19
+ width = crop.width;
20
+ height = crop.height;
21
+ if (crop.coordinates) {
22
+ const { x1, y1, x2, y2 } = crop.coordinates;
23
+ params.set('cc', [x1, y1, x2, y2].join(','));
24
+ }
25
+ else if (media.focalPoint)
26
+ params.set('rxy', `${media.focalPoint.left},${media.focalPoint.top}`);
27
+ }
28
+ else if (media.focalPoint)
29
+ params.set('rxy', `${media.focalPoint.left},${media.focalPoint.top}`);
30
+ const query = params.toString();
31
+ return {
32
+ src: query ? `${media.url}?${query}` : media.url,
33
+ width,
34
+ height,
35
+ alt: options.alt ?? media.name,
36
+ };
37
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A Next.js App Router front end for a crombolo site (story 208).
3
+ *
4
+ * `createCrombolo()` wraps the typed Delivery API client for server
5
+ * components: answers kept in Next's cache and tagged by the content they hold,
6
+ * drafts in Draft Mode, a member's own reads, route lookup as a `Result`, and
7
+ * Next's `Metadata` from a page's search and sharing details.
8
+ * docs/delivery-api.md, "A Next.js front end".
9
+ */
10
+ import { type Result } from '@crombolo/common';
11
+ import { type ContentItem, type ContentQuery, type DeliveryClient, type DeliverySchema, type RequestOptions } from '@crombolo/delivery-client';
12
+ import type { Metadata } from 'next';
13
+ import type { CacheAdapter } from './cache.ts';
14
+ import { type MemberSession } from './session.ts';
15
+ export { assertSafeLocation, type FieldError, isOk, isOutcome, type MatchHandlers, Ok, Outcome, type OutcomeStatus, ok, Result, } from '@crombolo/common';
16
+ export type { CacheAdapter, KeptAnswer } from './cache.ts';
17
+ export { SITE_TAG, tagOf, tagsFor } from './cache.ts';
18
+ export type { MemberSession } from './session.ts';
19
+ export interface CromboloOptions {
20
+ /** The CMS's address, such as `https://cms.example.com`. */
21
+ url: string;
22
+ /** The Delivery API key, for a site whose API is not public, and for preview. */
23
+ apiKey?: string;
24
+ /** Next's cache, from `./cache-16` or `./cache-15`; without one, every read revalidates by ETag. */
25
+ cache?: CacheAdapter;
26
+ /** `etag` revalidates every read rather than keeping it until the webhook clears it. */
27
+ revalidate?: 'tags' | 'etag';
28
+ /** The hostname routes resolve for, as the site's domains pick a root and culture. */
29
+ host?: string;
30
+ culture?: string;
31
+ startItem?: string;
32
+ /** `CROMBOLO_NEXT_SECRET`, which seals a member's session; read from the environment when unset. */
33
+ secret?: string;
34
+ /** The Delivery API's path, when the site serves it somewhere else. */
35
+ apiPath?: string;
36
+ }
37
+ /** What a page lookup can answer instead of a page. */
38
+ export type PageStatus = 301 | 302 | 303 | 307 | 308 | 401 | 403 | 404;
39
+ /** The reads a page makes, through whichever client the request calls for. */
40
+ export interface CromboloReads<Schema extends DeliverySchema = DeliverySchema> {
41
+ readonly client: DeliveryClient<Schema>;
42
+ /** The page at an address, a redirect, or a refusal, as the site would answer it. */
43
+ page(path: string, options?: {
44
+ host?: string;
45
+ expand?: string;
46
+ }): Promise<Result<ContentItem<Schema>, PageStatus>>;
47
+ /** `page()` ended with Next's own `redirect`, `permanentRedirect`, `notFound`, `unauthorized` or `forbidden`. */
48
+ resolvePage(path: string, options?: {
49
+ host?: string;
50
+ expand?: string;
51
+ }): Promise<ContentItem<Schema>>;
52
+ /** The site's not-found page for a hostname, when it has one. */
53
+ notFoundPage(options?: {
54
+ host?: string;
55
+ }): Promise<ContentItem<Schema> | undefined>;
56
+ item: DeliveryClient<Schema>['item'];
57
+ items: DeliveryClient<Schema>['items'];
58
+ content: DeliveryClient<Schema>['content'];
59
+ mediaItem: DeliveryClient<Schema>['mediaItem'];
60
+ media: DeliveryClient<Schema>['media'];
61
+ menu: DeliveryClient<Schema>['menu'];
62
+ search: DeliveryClient<Schema>['search'];
63
+ form: DeliveryClient<Schema>['form'];
64
+ sitemap: DeliveryClient<Schema>['sitemap'];
65
+ }
66
+ export interface Crombolo<Schema extends DeliverySchema = DeliverySchema> extends CromboloReads<Schema> {
67
+ readonly options: CromboloOptions;
68
+ /** Reads revalidated by ETag on every call, for a page rendered per request. */
69
+ readonly fresh: CromboloReads<Schema>;
70
+ /**
71
+ * The signed-in member's reads, never kept, or undefined for a visitor.
72
+ * Reads the session cookie, so the page renders per request.
73
+ */
74
+ member(): Promise<(CromboloReads<Schema> & {
75
+ session: MemberSession;
76
+ }) | undefined>;
77
+ /** Next's `Metadata` from a page's search and sharing details (`seo: true`). */
78
+ metadata(item: ContentItem<Schema> | undefined, extra?: Metadata): Metadata;
79
+ /** Clears every kept answer holding these keys. */
80
+ revalidate(keys: readonly string[]): void;
81
+ }
82
+ export declare function createCrombolo<Schema extends DeliverySchema = DeliverySchema>(options: CromboloOptions): Crombolo<Schema>;
83
+ /** Next's `Metadata` from a page's `seo` details; `extra` wins field by field. Pure. */
84
+ export declare function metadataOf(item: {
85
+ name: string;
86
+ seo?: ContentItem['seo'];
87
+ } | undefined, extra?: Metadata): Metadata;
88
+ export type { ApiBlockGrid, ApiBlockGridItem, ApiBlockList, ApiBlockListItem, ApiContent, ApiContentReference, ApiCrop, ApiElement, ApiFocalPoint, ApiLink, ApiMedia, ApiMediaReference, ApiMediaWithCrops, ApiMenu, ApiMenuItem, ApiRichText, ApiRoute, ApiSeo, MediaItem, Paged, SearchHit, } from '@crombolo/delivery-client';
89
+ export { isExpanded } from '@crombolo/delivery-client';
90
+ export type { ContentItem, ContentQuery, DeliverySchema, RequestOptions };