@bettercms-ai/astro 0.24.0 → 0.25.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/README.md +227 -227
- package/components/BcmsBlocks.astro +640 -640
- package/components/BcmsButton.astro +44 -44
- package/components/BcmsField.astro +39 -30
- package/components/BcmsForm.astro +360 -360
- package/components/BcmsImage.astro +131 -112
- package/components/BcmsLive.astro +66 -66
- package/components/BcmsLiveBlocks.astro +72 -72
- package/components/BcmsSearch.astro +23 -23
- package/components/BcmsVisualEditing.astro +26 -26
- package/dist/bindings.d.ts +15 -1
- package/dist/bindings.js +62 -1
- package/dist/bindings.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware-entry.js.map +1 -1
- package/dist/routes/draft-disable.js.map +1 -1
- package/dist/routes/draft-enable.js.map +1 -1
- package/dist/runtime.js.map +1 -1
- package/env.d.ts +35 -35
- package/package.json +86 -86
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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: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>
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* <BcmsVisualEditing> — click-to-edit overlay for draft preview. Alt+Click any value
|
|
4
|
-
* that carries stega provenance to open the matching dashboard editor on that field.
|
|
5
|
-
* Render it only in draft mode.
|
|
6
|
-
*
|
|
7
|
-
* import config from "bettercms:config";
|
|
8
|
-
* {Astro.locals.bcms?.draft && <BcmsVisualEditing studioUrl={config.studioUrl} />}
|
|
9
|
-
*
|
|
10
|
-
* The decoder is NOT inlined here. It is one script served from the dashboard origin
|
|
11
|
-
* (`/bcms-visual-editing.js`), shared with @bettercms-ai/next and with plain-HTML sites
|
|
12
|
-
* that install it by hand — a customer build is un-upgradable, so a copy baked into one
|
|
13
|
-
* could never be fixed. Requires the read client to be in draft perspective with
|
|
14
|
-
* `stega: true`; on a published build there is no provenance and the script is inert.
|
|
15
|
-
*/
|
|
16
|
-
interface Props {
|
|
17
|
-
studioUrl?: string;
|
|
18
|
-
}
|
|
19
|
-
// `bettercms.ai` is the studio origin — the one the dashboard's integration snippets hand out
|
|
20
|
-
// as `PUBLIC_BCMS_STUDIO_URL`, and the default of @bettercms-ai/next's <VisualEditing>.
|
|
21
|
-
// `demo.bettercms.ai` is a stale alias; `dashboard.bettercms.ai` resolves but 404s.
|
|
22
|
-
const { studioUrl = "https://bettercms.ai" } = Astro.props;
|
|
23
|
-
const src = `${studioUrl.replace(/\/+$/, "")}/bcms-visual-editing.js`;
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
<script is:inline src={src} data-bcms-studio={studioUrl} defer></script>
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <BcmsVisualEditing> — click-to-edit overlay for draft preview. Alt+Click any value
|
|
4
|
+
* that carries stega provenance to open the matching dashboard editor on that field.
|
|
5
|
+
* Render it only in draft mode.
|
|
6
|
+
*
|
|
7
|
+
* import config from "bettercms:config";
|
|
8
|
+
* {Astro.locals.bcms?.draft && <BcmsVisualEditing studioUrl={config.studioUrl} />}
|
|
9
|
+
*
|
|
10
|
+
* The decoder is NOT inlined here. It is one script served from the dashboard origin
|
|
11
|
+
* (`/bcms-visual-editing.js`), shared with @bettercms-ai/next and with plain-HTML sites
|
|
12
|
+
* that install it by hand — a customer build is un-upgradable, so a copy baked into one
|
|
13
|
+
* could never be fixed. Requires the read client to be in draft perspective with
|
|
14
|
+
* `stega: true`; on a published build there is no provenance and the script is inert.
|
|
15
|
+
*/
|
|
16
|
+
interface Props {
|
|
17
|
+
studioUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
// `bettercms.ai` is the studio origin — the one the dashboard's integration snippets hand out
|
|
20
|
+
// as `PUBLIC_BCMS_STUDIO_URL`, and the default of @bettercms-ai/next's <VisualEditing>.
|
|
21
|
+
// `demo.bettercms.ai` is a stale alias; `dashboard.bettercms.ai` resolves but 404s.
|
|
22
|
+
const { studioUrl = "https://bettercms.ai" } = Astro.props;
|
|
23
|
+
const src = `${studioUrl.replace(/\/+$/, "")}/bcms-visual-editing.js`;
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<script is:inline src={src} data-bcms-studio={studioUrl} defer></script>
|