@bettercms-ai/astro 0.24.0 → 0.26.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.
@@ -1,112 +1,131 @@
1
- ---
2
- /**
3
- * <BcmsImage> — renders an optimized image via the BetterCMS media transform
4
- * endpoint, with a 1x/2x srcset. Accepts an asset id, a MediaAsset, a URL, or an image
5
- * VALUE (`{ url, altText, focal?, crop? }`) — whose per-placement crop and focal point are
6
- * applied by image-url itself (`rect`, and fx/fy inside the crop), the same as on every lane.
7
- */
8
- import config from "bettercms:config";
9
- import imageUrlBuilder, {
10
- focalInCrop,
11
- normalizeImageCrop,
12
- normalizeImageFocal,
13
- objectPosition,
14
- type ImageSource,
15
- type ImageFormat,
16
- type ImageFit,
17
- } from "@bettercms-ai/image-url";
18
-
19
- interface Props {
20
- src: ImageSource;
21
- alt?: string;
22
- width?: number;
23
- height?: number;
24
- format?: ImageFormat;
25
- quality?: number;
26
- fit?: ImageFit;
27
- sizes?: string;
28
- loading?: "lazy" | "eager";
29
- class?: string;
30
- /**
31
- * Focal point override, 0–1. Normally omitted — the asset carries its own and the
32
- * server applies it. Pass a MediaAsset with focalX/focalY as `src` and it is picked
33
- * up automatically.
34
- */
35
- focalX?: number | null;
36
- focalY?: number | null;
37
- /** Any other attribute (e.g. `data-bcms-field`) is forwarded to the <img>. */
38
- [attr: string]: unknown;
39
- }
40
-
41
- const {
42
- src,
43
- alt = "",
44
- width,
45
- height,
46
- format = "webp",
47
- quality,
48
- fit,
49
- sizes,
50
- loading = "lazy",
51
- class: className,
52
- focalX,
53
- focalY,
54
- style,
55
- ...rest
56
- } = Astro.props;
57
-
58
- // An explicit prop wins; then the value's own `focal`; then the asset's focalX/focalY. The
59
- // explicit prop is taken as given (window-relative when the value is cropped); the other two are
60
- // SOURCE-relative, so with a crop they are re-expressed inside it — image-url's focalInCrop.
61
- const asset = (typeof src === "object" && src !== null ? src : {}) as Record<string, unknown>;
62
- const explicit = focalX != null || focalY != null;
63
- const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
64
- const stored =
65
- normalizeImageFocal(asset.focal) ??
66
- (num(asset.focalX) != null || num(asset.focalY) != null
67
- ? { x: num(asset.focalX) ?? 0.5, y: num(asset.focalY) ?? 0.5 }
68
- : null);
69
- const inWindow = stored ? focalInCrop(stored, normalizeImageCrop(asset.crop)) : null;
70
- const fx = explicit ? focalX : inWindow?.x;
71
- const fy = explicit ? focalY : inWindow?.y;
72
- const hasFocal = fx != null || fy != null;
73
-
74
- const builder = imageUrlBuilder({ baseUrl: config.mediaUrl });
75
- const make = () => {
76
- let b = builder(src);
77
- if (width) b = b.width(width);
78
- if (height) b = b.height(height);
79
- if (format) b = b.format(format);
80
- if (quality) b = b.quality(quality);
81
- if (fit) b = b.fit(fit);
82
- if (hasFocal) b = b.focal(fx ?? 0.5, fy ?? 0.5);
83
- return b;
84
- };
85
-
86
- const src1x = make().url();
87
- // A pixel width is the author's size for THIS image, so it goes on as a style (a site rule like
88
- // `img { width: 100% }` beats a bare attribute) — paired with max-width so it never overflows a
89
- // narrower column, and height:auto so shrinking keeps the aspect ratio.
90
- const widthStyle = width ? `width:${width}px;max-width:100%;height:auto` : "";
91
- const src2x = make().dpr(2).url();
92
-
93
- // The CSS half of the same idea, and it is free. When only one dimension is pinned the
94
- // server does not crop, so the browser does — and `object-position` keeps the subject in
95
- // frame there too, but only if the stylesheet actually set `object-fit: cover`. Emitting
96
- // it unconditionally is harmless: `object-position` is inert without `object-fit`.
97
- const focalStyle = hasFocal ? `object-position:${objectPosition({ x: fx, y: fy })}` : "";
98
- const mergedStyle = [widthStyle, focalStyle, typeof style === "string" ? style : ""].filter(Boolean).join(";");
99
- ---
100
-
101
- <img
102
- src={src1x}
103
- srcset={`${src1x} 1x, ${src2x} 2x`}
104
- alt={alt}
105
- width={width}
106
- height={height}
107
- sizes={sizes}
108
- loading={loading}
109
- class={className}
110
- style={mergedStyle || undefined}
111
- {...rest}
112
- />
1
+ ---
2
+ /**
3
+ * <BcmsImage> — renders an optimized image via the BetterCMS media transform
4
+ * endpoint, with a 1x/2x srcset. Accepts an asset id, a MediaAsset, a URL, or an image
5
+ * VALUE (`{ url, altText, focal?, crop? }`) — whose per-placement crop and focal point are
6
+ * applied by image-url itself (`rect`, and fx/fy inside the crop), the same as on every lane.
7
+ *
8
+ * Given `path`, the <img> declares its field (`data-bcms-field`, kind `image`), so the visual
9
+ * editor binds it and a publish writes the new picture into it — and the page reads the value
10
+ * through this component, so the next build renders that same picture:
11
+ *
12
+ * <BcmsImage path="hero.image" src={page.hero.image} sizes="(min-width: 800px) 50vw, 100vw" />
13
+ *
14
+ * With `sizes` and no `width` the srcset is a WIDTH ladder (never wider than the picture), which
15
+ * is what `sizes` selects from. The value's `altText` is the alt unless `alt` says otherwise.
16
+ * The same markup as @bettercms-ai/next's <BcmsImage>.
17
+ */
18
+ import config from "@bettercms-ai/astro/public-config";
19
+ import imageUrlBuilder, {
20
+ focalInCrop,
21
+ normalizeImageCrop,
22
+ normalizeImageFocal,
23
+ objectPosition,
24
+ type ImageSource,
25
+ type ImageFormat,
26
+ type ImageFit,
27
+ } from "@bettercms-ai/image-url";
28
+
29
+ interface Props {
30
+ src: ImageSource;
31
+ alt?: string;
32
+ width?: number;
33
+ height?: number;
34
+ format?: ImageFormat;
35
+ quality?: number;
36
+ fit?: ImageFit;
37
+ sizes?: string;
38
+ loading?: "lazy" | "eager";
39
+ class?: string;
40
+ /**
41
+ * Focal point override, 0–1. Normally omitted — the asset carries its own and the
42
+ * server applies it. Pass a MediaAsset with focalX/focalY as `src` and it is picked
43
+ * up automatically.
44
+ */
45
+ focalX?: number | null;
46
+ focalY?: number | null;
47
+ /** The field this image renders, in the editor grammar (`cover`, `hero.image`). */
48
+ path?: string;
49
+ /** Any other attribute (e.g. `data-bcms-field`) is forwarded to the <img>. */
50
+ [attr: string]: unknown;
51
+ }
52
+
53
+ const {
54
+ src,
55
+ alt,
56
+ width,
57
+ height,
58
+ format = "webp",
59
+ quality,
60
+ fit,
61
+ sizes,
62
+ loading = "lazy",
63
+ class: className,
64
+ focalX,
65
+ focalY,
66
+ path,
67
+ style,
68
+ ...rest
69
+ } = Astro.props;
70
+
71
+ // An explicit prop wins; then the value's own `focal`; then the asset's focalX/focalY. The
72
+ // explicit prop is taken as given (window-relative when the value is cropped); the other two are
73
+ // SOURCE-relative, so with a crop they are re-expressed inside it — image-url's focalInCrop.
74
+ const asset = (typeof src === "object" && src !== null ? src : {}) as Record<string, unknown>;
75
+ const explicit = focalX != null || focalY != null;
76
+ const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
77
+ const stored =
78
+ normalizeImageFocal(asset.focal) ??
79
+ (num(asset.focalX) != null || num(asset.focalY) != null
80
+ ? { x: num(asset.focalX) ?? 0.5, y: num(asset.focalY) ?? 0.5 }
81
+ : null);
82
+ const inWindow = stored ? focalInCrop(stored, normalizeImageCrop(asset.crop)) : null;
83
+ const fx = explicit ? focalX : inWindow?.x;
84
+ const fy = explicit ? focalY : inWindow?.y;
85
+ const hasFocal = fx != null || fy != null;
86
+
87
+ const builder = imageUrlBuilder({ baseUrl: config.mediaUrl });
88
+ const make = () => {
89
+ let b = builder(src);
90
+ if (width) b = b.width(width);
91
+ if (height) b = b.height(height);
92
+ if (format) b = b.format(format);
93
+ if (quality) b = b.quality(quality);
94
+ if (fit) b = b.fit(fit);
95
+ if (hasFocal) b = b.focal(fx ?? 0.5, fy ?? 0.5);
96
+ return b;
97
+ };
98
+
99
+ const src1x = make().url();
100
+ // The widths a `sizes` image offers: phone to desktop, and never an upscale of the picture.
101
+ const LADDER = [480, 768, 1080, 1440, 1920];
102
+ const own = num(asset.width);
103
+ const ladder = own ? [...new Set([...LADDER.filter((w) => w < own), Math.min(own, LADDER[LADDER.length - 1])])] : LADDER;
104
+ // A pixel width is the author's size for THIS image, so it goes on as a style (a site rule like
105
+ // `img { width: 100% }` beats a bare attribute) — paired with max-width so it never overflows a
106
+ // narrower column, and height:auto so shrinking keeps the aspect ratio.
107
+ const widthStyle = width ? `width:${width}px;max-width:100%;height:auto` : "";
108
+ const src2x = make().dpr(2).url();
109
+
110
+ // The CSS half of the same idea, and it is free. When only one dimension is pinned the
111
+ // server does not crop, so the browser does — and `object-position` keeps the subject in
112
+ // frame there too, but only if the stylesheet actually set `object-fit: cover`. Emitting
113
+ // it unconditionally is harmless: `object-position` is inert without `object-fit`.
114
+ const focalStyle = hasFocal ? `object-position:${objectPosition({ x: fx, y: fy })}` : "";
115
+ const mergedStyle = [widthStyle, focalStyle, typeof style === "string" ? style : ""].filter(Boolean).join(";");
116
+ ---
117
+
118
+ <img
119
+ src={src1x}
120
+ srcset={sizes && !width ? ladder.map((w) => `${make().width(w).url()} ${w}w`).join(", ") : `${src1x} 1x, ${src2x} 2x`}
121
+ alt={alt ?? (typeof asset.altText === "string" ? asset.altText : typeof asset.alt === "string" ? asset.alt : "")}
122
+ data-bcms-field={path}
123
+ data-bcms-kind={path ? "image" : undefined}
124
+ width={width}
125
+ height={height}
126
+ sizes={sizes}
127
+ loading={loading}
128
+ class={className}
129
+ style={mergedStyle || undefined}
130
+ {...rest}
131
+ />
@@ -1,66 +1,66 @@
1
- ---
2
- /**
3
- * <BcmsLive> — opens the BetterCMS delivery live SSE stream in the browser and
4
- * keeps the page current. Drop it once, e.g. in your layout. Build `src` with
5
- * `liveSrc({ apiUrl, workspace, apiKey })` from `@bettercms-ai/astro` (use a
6
- * content:read key — it is browser-exposed).
7
- *
8
- * Every change frame is dispatched on `window` as a cancelable `bcms:change`
9
- * CustomEvent whose `detail` is the frame — including `detail.doc`, the finished
10
- * delivery document the server already hydrated for you. Patch the DOM from it
11
- * and call `preventDefault()` to keep the page as it is:
12
- *
13
- * window.addEventListener("bcms:change", (e) => {
14
- * if (e.detail.slug !== "home") return;
15
- * document.querySelector("h1").textContent = e.detail.doc.data.title;
16
- * e.preventDefault(); // handled — skip the reload
17
- * });
18
- *
19
- * Nothing handles it → the page reloads, as it always did. That fallback is why
20
- * this is safe to drop into an existing site unchanged.
21
- *
22
- * `tags` narrows which changes count at all. Without it a publish anywhere in the
23
- * workspace reloads every open tab, including pages that render none of it.
24
- */
25
- interface Props {
26
- src: string;
27
- /** Only react to changes carrying one of these tags. Omit = react to all. */
28
- tags?: string[];
29
- }
30
- const { src, tags } = Astro.props;
31
- ---
32
-
33
- <script is:inline define:vars={{ src, tags }}>
34
- try {
35
- const es = new EventSource(src);
36
- es.addEventListener("change", (e) => {
37
- let detail;
38
- try {
39
- detail = JSON.parse(e.data);
40
- } catch {
41
- return;
42
- }
43
- if (tags?.length && !detail.tags?.some((t) => tags.includes(t))) return;
44
- const handled = !window.dispatchEvent(
45
- new CustomEvent("bcms:change", { detail, cancelable: true }),
46
- );
47
- if (!handled) window.location.reload();
48
- });
49
- } catch (e) {
50
- /* EventSource unsupported — no live refresh */
51
- }
52
-
53
- // Soft refresh for the BetterCMS canvas. The editor knows which of its writes are
54
- // STRUCTURAL and asks for a re-render only for those; the ack is what tells it this page
55
- // answered, so it does not remount the iframe (which throws away an inline text session).
56
- // Same acceptance predicate as the Next draft bridge: own origin, and the parent frame.
57
- if (window.parent !== window) {
58
- window.addEventListener("message", (e) => {
59
- if (e.origin !== location.origin || e.source !== window.parent) return;
60
- if (e.data?.type !== "bcms:refresh") return;
61
- // Ack BEFORE reloading — after it, this frame is gone and the editor has timed out.
62
- window.parent.postMessage({ type: "bcms:refreshed" }, location.origin);
63
- window.location.reload();
64
- });
65
- }
66
- </script>
1
+ ---
2
+ /**
3
+ * <BcmsLive> — opens the BetterCMS delivery live SSE stream in the browser and
4
+ * keeps the page current. Drop it once, e.g. in your layout. Build `src` with
5
+ * `liveSrc({ apiUrl, workspace, apiKey })` from `@bettercms-ai/astro` (use a
6
+ * content:read key — it is browser-exposed).
7
+ *
8
+ * Every change frame is dispatched on `window` as a cancelable `bcms:change`
9
+ * CustomEvent whose `detail` is the frame — including `detail.doc`, the finished
10
+ * delivery document the server already hydrated for you. Patch the DOM from it
11
+ * and call `preventDefault()` to keep the page as it is:
12
+ *
13
+ * window.addEventListener("bcms:change", (e) => {
14
+ * if (e.detail.slug !== "home") return;
15
+ * document.querySelector("h1").textContent = e.detail.doc.data.title;
16
+ * e.preventDefault(); // handled — skip the reload
17
+ * });
18
+ *
19
+ * Nothing handles it → the page reloads, as it always did. That fallback is why
20
+ * this is safe to drop into an existing site unchanged.
21
+ *
22
+ * `tags` narrows which changes count at all. Without it a publish anywhere in the
23
+ * workspace reloads every open tab, including pages that render none of it.
24
+ */
25
+ interface Props {
26
+ src: string;
27
+ /** Only react to changes carrying one of these tags. Omit = react to all. */
28
+ tags?: string[];
29
+ }
30
+ const { src, tags } = Astro.props;
31
+ ---
32
+
33
+ <script is:inline define:vars={{ src, tags }}>
34
+ try {
35
+ const es = new EventSource(src);
36
+ es.addEventListener("change", (e) => {
37
+ let detail;
38
+ try {
39
+ detail = JSON.parse(e.data);
40
+ } catch {
41
+ return;
42
+ }
43
+ if (tags?.length && !detail.tags?.some((t) => tags.includes(t))) return;
44
+ const handled = !window.dispatchEvent(
45
+ new CustomEvent("bcms:change", { detail, cancelable: true }),
46
+ );
47
+ if (!handled) window.location.reload();
48
+ });
49
+ } catch (e) {
50
+ /* EventSource unsupported — no live refresh */
51
+ }
52
+
53
+ // Soft refresh for the BetterCMS canvas. The editor knows which of its writes are
54
+ // STRUCTURAL and asks for a re-render only for those; the ack is what tells it this page
55
+ // answered, so it does not remount the iframe (which throws away an inline text session).
56
+ // Same acceptance predicate as the Next draft bridge: own origin, and the parent frame.
57
+ if (window.parent !== window) {
58
+ window.addEventListener("message", (e) => {
59
+ if (e.origin !== location.origin || e.source !== window.parent) return;
60
+ if (e.data?.type !== "bcms:refresh") return;
61
+ // Ack BEFORE reloading — after it, this frame is gone and the editor has timed out.
62
+ window.parent.postMessage({ type: "bcms:refreshed" }, location.origin);
63
+ window.location.reload();
64
+ });
65
+ }
66
+ </script>
@@ -1,72 +1,72 @@
1
- ---
2
- /**
3
- * <BcmsLiveBlocks> — <BcmsBlocks>, but as a hydrated island that speaks the visual editor's
4
- * draft bridge (E5 / FLO-1178).
5
- *
6
- * WHY IT IS NOT A .astro RENDERER. An .astro component is server-only: once the page is built
7
- * there is nothing on the client to hand a new document to, so an Astro site can only show the
8
- * editor a STATIC build and the canvas falls back to the platform's approximate renderer for
9
- * every draft. The bridge needs a live component tree, and the only one that already exists,
10
- * is at parity with this package, and speaks the protocol is <BcmsDraftBridge> from
11
- * @bettercms-ai/next — which imports nothing from `next/*` (React and @bettercms-ai/* only),
12
- * so it mounts here unchanged. Reused rather than reimplemented: one protocol, one twin to
13
- * keep in step instead of two.
14
- *
15
- * RUNTIME: Preact + `compat`, verified by the P2-1 spike (see ../SPIKE.md — SSR and hydration
16
- * of a heading + PortableText richtext + BcmsForm tree, both clean). The cost is real and worth
17
- * naming: preact plus its compat aliases on the island (~10 kB of runtime, so `react` itself is
18
- * never bundled even though @bettercms-ai/ui declares React peers), and `@bettercms-ai/next` as a
19
- * dependency of this package.
20
- *
21
- * 🔴 THE CLASS NAMES ARE THE REACT GRAMMAR, NOT THIS PACKAGE'S. <BcmsBlocks> in @bettercms-ai/next
22
- * emits `bcms-block bcms-block--text`, `bcms-button--primary`, `bcms-column`; BcmsBlocks.astro
23
- * emits `bcms-text`, `bcms-btn-primary`, `bcms-cols`. Same markup shape, different hooks — a
24
- * stylesheet written against the .astro grammar styles NOTHING here, and swapping a page from
25
- * <BcmsBlocks> to <BcmsLiveBlocks> unstyles it. Either cover both spellings (the starters use
26
- * `:is()` for exactly this) or read the React classes off @bettercms-ai/next's src/blocks.tsx.
27
- *
28
- * The consumer supplies the renderer, because an integration cannot be added from a component:
29
- *
30
- * // astro.config.mjs
31
- * import preact from "@astrojs/preact";
32
- * export default defineConfig({ integrations: [bettercms(), preact({ compat: true })] });
33
- *
34
- * ---
35
- * import BcmsLiveBlocks from "@bettercms-ai/astro/components/BcmsLiveBlocks.astro";
36
- * const page = await getPage(Astro.params.slug);
37
- * ---
38
- * <BcmsLiveBlocks slug={page.slug} blocks={page.blocks} forms={forms} />
39
- *
40
- * Standalone the island renders exactly what <BcmsBlocks> renders (inside the
41
- * `data-bcms-canvas="bridge"` marker <div> that <BcmsDraftBridge> emits); the bridge only
42
- * wakes when the editor frames the page and pushes a draft. This only helps pages whose body
43
- * IS a block tree — a hand-written .astro layout is unreachable from here by construction.
44
- */
45
- import { BcmsDraftBridge } from "@bettercms-ai/next/draft-bridge";
46
- import type { ContentBlock } from "@bettercms-ai/types";
47
- import type { DeliveryComponent, DeliveryForm } from "@bettercms-ai/sdk";
48
-
49
- interface Props {
50
- /** The page's slug. A pushed draft is applied only when its slug matches. */
51
- slug: string;
52
- blocks: ContentBlock[];
53
- forms?: DeliveryForm[];
54
- components?: DeliveryComponent[];
55
- turnstileSiteKey?: string;
56
- apiBase?: string;
57
- class?: string;
58
- }
59
-
60
- const { slug, blocks, forms, components, turnstileSiteKey, apiBase, class: className } = Astro.props;
61
- ---
62
-
63
- <BcmsDraftBridge
64
- client:load
65
- slug={slug}
66
- blocks={blocks}
67
- forms={forms}
68
- components={components}
69
- turnstileSiteKey={turnstileSiteKey}
70
- apiBase={apiBase}
71
- className={className}
72
- />
1
+ ---
2
+ /**
3
+ * <BcmsLiveBlocks> — <BcmsBlocks>, but as a hydrated island that speaks the visual editor's
4
+ * draft bridge (E5 / FLO-1178).
5
+ *
6
+ * WHY IT IS NOT A .astro RENDERER. An .astro component is server-only: once the page is built
7
+ * there is nothing on the client to hand a new document to, so an Astro site can only show the
8
+ * editor a STATIC build and the canvas falls back to the platform's approximate renderer for
9
+ * every draft. The bridge needs a live component tree, and the only one that already exists,
10
+ * is at parity with this package, and speaks the protocol is <BcmsDraftBridge> from
11
+ * @bettercms-ai/next — which imports nothing from `next/*` (React and @bettercms-ai/* only),
12
+ * so it mounts here unchanged. Reused rather than reimplemented: one protocol, one twin to
13
+ * keep in step instead of two.
14
+ *
15
+ * RUNTIME: Preact + `compat`, verified by the P2-1 spike (see ../SPIKE.md — SSR and hydration
16
+ * of a heading + PortableText richtext + BcmsForm tree, both clean). The cost is real and worth
17
+ * naming: preact plus its compat aliases on the island (~10 kB of runtime, so `react` itself is
18
+ * never bundled even though @bettercms-ai/ui declares React peers), and `@bettercms-ai/next` as a
19
+ * dependency of this package.
20
+ *
21
+ * 🔴 THE CLASS NAMES ARE THE REACT GRAMMAR, NOT THIS PACKAGE'S. <BcmsBlocks> in @bettercms-ai/next
22
+ * emits `bcms-block bcms-block--text`, `bcms-button--primary`, `bcms-column`; BcmsBlocks.astro
23
+ * emits `bcms-text`, `bcms-btn-primary`, `bcms-cols`. Same markup shape, different hooks — a
24
+ * stylesheet written against the .astro grammar styles NOTHING here, and swapping a page from
25
+ * <BcmsBlocks> to <BcmsLiveBlocks> unstyles it. Either cover both spellings (the starters use
26
+ * `:is()` for exactly this) or read the React classes off @bettercms-ai/next's src/blocks.tsx.
27
+ *
28
+ * The consumer supplies the renderer, because an integration cannot be added from a component:
29
+ *
30
+ * // astro.config.mjs
31
+ * import preact from "@astrojs/preact";
32
+ * export default defineConfig({ integrations: [bettercms(), preact({ compat: true })] });
33
+ *
34
+ * ---
35
+ * import BcmsLiveBlocks from "@bettercms-ai/astro/components/BcmsLiveBlocks.astro";
36
+ * const page = await getPage(Astro.params.slug);
37
+ * ---
38
+ * <BcmsLiveBlocks slug={page.slug} blocks={page.blocks} forms={forms} />
39
+ *
40
+ * Standalone the island renders exactly what <BcmsBlocks> renders (inside the
41
+ * `data-bcms-canvas="bridge"` marker <div> that <BcmsDraftBridge> emits); the bridge only
42
+ * wakes when the editor frames the page and pushes a draft. This only helps pages whose body
43
+ * IS a block tree — a hand-written .astro layout is unreachable from here by construction.
44
+ */
45
+ import { BcmsDraftBridge } from "@bettercms-ai/next/draft-bridge";
46
+ import type { ContentBlock } from "@bettercms-ai/types";
47
+ import type { DeliveryComponent, DeliveryForm } from "@bettercms-ai/sdk";
48
+
49
+ interface Props {
50
+ /** The page's slug. A pushed draft is applied only when its slug matches. */
51
+ slug: string;
52
+ blocks: ContentBlock[];
53
+ forms?: DeliveryForm[];
54
+ components?: DeliveryComponent[];
55
+ turnstileSiteKey?: string;
56
+ apiBase?: string;
57
+ class?: string;
58
+ }
59
+
60
+ const { slug, blocks, forms, components, turnstileSiteKey, apiBase, class: className } = Astro.props;
61
+ ---
62
+
63
+ <BcmsDraftBridge
64
+ client:load
65
+ slug={slug}
66
+ blocks={blocks}
67
+ forms={forms}
68
+ components={components}
69
+ turnstileSiteKey={turnstileSiteKey}
70
+ apiBase={apiBase}
71
+ className={className}
72
+ />
@@ -1,23 +1,23 @@
1
- ---
2
- /**
3
- * <BcmsSearch> — an instant, typo-tolerant site-search box for Astro.
4
- *
5
- * Renders the search container and loads the hosted widget bundle, so the markup
6
- * and behaviour are identical to the universal HTML embed (DRY — one widget, one
7
- * place). No API key — `project` (from the dashboard's Site Search settings) is
8
- * the only scope. Styling is class-driven (`bcms-search-*`); the host can override.
9
- */
10
- import config from "bettercms:config";
11
-
12
- interface Props {
13
- project: string;
14
- placeholder?: string;
15
- class?: string;
16
- }
17
-
18
- const { project, placeholder = "Search…", class: className } = Astro.props;
19
- const widgetSrc = `${config.apiUrl}/widgets/search.js`;
20
- ---
21
-
22
- <div data-bcms-search data-project={project} data-placeholder={placeholder} class={className}></div>
23
- <script is:inline src={widgetSrc} async></script>
1
+ ---
2
+ /**
3
+ * <BcmsSearch> — an instant, typo-tolerant site-search box for Astro.
4
+ *
5
+ * Renders the search container and loads the hosted widget bundle, so the markup
6
+ * and behaviour are identical to the universal HTML embed (DRY — one widget, one
7
+ * place). No API key — `project` (from the dashboard's Site Search settings) is
8
+ * the only scope. Styling is class-driven (`bcms-search-*`); the host can override.
9
+ */
10
+ import config from "bettercms:config";
11
+
12
+ interface Props {
13
+ project: string;
14
+ placeholder?: string;
15
+ class?: string;
16
+ }
17
+
18
+ const { project, placeholder = "Search…", class: className } = Astro.props;
19
+ const widgetSrc = `${config.apiUrl}/widgets/search.js`;
20
+ ---
21
+
22
+ <div data-bcms-search data-project={project} data-placeholder={placeholder} class={className}></div>
23
+ <script is:inline src={widgetSrc} async></script>