@bettercms-ai/astro 0.22.1 → 0.23.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/components/BcmsBlocks.astro +28 -8
- package/components/BcmsForm.astro +82 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* actually contains that block (mirrors render-page.ts's `ctx.flags` gating).
|
|
27
27
|
*/
|
|
28
28
|
import type { ContentBlock } from "@bettercms-ai/types";
|
|
29
|
-
import { blockStyleToCss, blockStyleToInnerCss, splitButtonStyle, buttonIconOf, buttonIconSvg, buttonVariantOf, componentInstanceAddresses, headingPaintCss, withButtonLookProps } from "@bettercms-ai/types";
|
|
29
|
+
import { RESPONSIVE_CSS, blockStyleToCss, hideOnAttr, usesHideOn, visibleBlocks, windowState, blockStyleToInnerCss, splitButtonStyle, buttonIconOf, buttonIconSvg, buttonVariantOf, componentInstanceAddresses, headingPaintCss, withButtonLookProps } from "@bettercms-ai/types";
|
|
30
30
|
import { bind, bindProps } from "../dist/bindings.js";
|
|
31
31
|
import type { DeliveryComponent, DeliveryForm } from "@bettercms-ai/sdk";
|
|
32
32
|
import { applyOverrides } from "@bettercms-ai/ui/resolve";
|
|
@@ -66,13 +66,19 @@ interface Props {
|
|
|
66
66
|
* Internal; do not pass. */
|
|
67
67
|
owner?: string | null;
|
|
68
68
|
class?: string;
|
|
69
|
+
/** The instant block windows (`block.window`) are read against. Defaults to the render time:
|
|
70
|
+
* the build for a static page, the request for a server-rendered one. */
|
|
71
|
+
now?: Date;
|
|
72
|
+
/** Draw the blocks outside their window too, marked `data-bcms-window="pending|expired"`, for
|
|
73
|
+
* a draft preview. Off, a block outside its window is not rendered, at any depth. */
|
|
74
|
+
showScheduled?: boolean;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
/** Matches render-page.ts and @bettercms-ai/next; a component tree deeper than this is a bug. */
|
|
72
78
|
const MAX_COMPONENT_DEPTH = 10;
|
|
73
79
|
|
|
74
80
|
const {
|
|
75
|
-
blocks = [],
|
|
81
|
+
blocks: all = [],
|
|
76
82
|
forms = [],
|
|
77
83
|
components = [],
|
|
78
84
|
turnstileSiteKey,
|
|
@@ -83,7 +89,13 @@ const {
|
|
|
83
89
|
fieldAddresses,
|
|
84
90
|
owner,
|
|
85
91
|
class: className,
|
|
92
|
+
now,
|
|
93
|
+
showScheduled = false,
|
|
86
94
|
} = Astro.props;
|
|
95
|
+
const at = now ?? new Date();
|
|
96
|
+
// The outermost render removes every block outside its window, at any depth, so each
|
|
97
|
+
// self-recursion below is handed children that are already the visible ones.
|
|
98
|
+
const blocks = showScheduled || nested ? all : visibleBlocks(all, at);
|
|
87
99
|
/** What `bind`/`bindProps` read. One object, so every call site cannot disagree. */
|
|
88
100
|
const bctx = { fieldAddresses };
|
|
89
101
|
const formMap = new Map(forms.map((f) => [f.id, f]));
|
|
@@ -204,12 +216,14 @@ function containsType(list: unknown, type: string, d = 0, chain: string[] = []):
|
|
|
204
216
|
|
|
205
217
|
const needsSlider = !nested && containsType(blocks, "slider");
|
|
206
218
|
const needsTabs = !nested && containsType(blocks, "tabs");
|
|
219
|
+
/** `RESPONSIVE_CSS` rides the page only when a block hides somewhere, so other pages are unchanged. */
|
|
220
|
+
const needsHideCss = !nested && usesHideOn([blocks, components]);
|
|
207
221
|
|
|
208
222
|
/** Props every self-recursion forwards unchanged. */
|
|
209
223
|
// `fieldAddresses` rides along: a slide or tab panel is still inside the SAME component
|
|
210
224
|
// instance, so its children keep that instance's addresses. The `component` case below
|
|
211
225
|
// overrides it with the NEW instance's map.
|
|
212
|
-
const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses };
|
|
226
|
+
const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses, now: at, showScheduled };
|
|
213
227
|
---
|
|
214
228
|
|
|
215
229
|
{/*
|
|
@@ -226,10 +240,13 @@ const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses
|
|
|
226
240
|
shipped once per nesting level to every visitor (FLO-1161).
|
|
227
241
|
*/}
|
|
228
242
|
<div class={className} data-bcms-page={nested ? undefined : ""} {...containerAttrs}>
|
|
243
|
+
{needsHideCss && <style set:html={RESPONSIVE_CSS} />}
|
|
229
244
|
{blocks.map((block) => {
|
|
230
245
|
const p = anyProps(block);
|
|
231
246
|
const style = styleAttr(block);
|
|
232
247
|
const inner = innerStyleAttr(block);
|
|
248
|
+
/** `data-bcms-hide`, a hook for `RESPONSIVE_CSS`; a block that hides somewhere gets the wrapper below. */
|
|
249
|
+
const hide = hideOnAttr(ownStyle(block)) || undefined;
|
|
233
250
|
/**
|
|
234
251
|
* 🔴 `data-bcms-block` — the Visual Editor hit-tests this to find sections. It rides the
|
|
235
252
|
* block's OWN root element; it does NOT get a wrapper.
|
|
@@ -251,7 +268,9 @@ const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses
|
|
|
251
268
|
* section's `padding` collides last-wins instead of adding, and a button's `text-align`
|
|
252
269
|
* would centre its label instead of the button).
|
|
253
270
|
*/
|
|
254
|
-
|
|
271
|
+
/** `showScheduled` only: which side of its window a drawn block is on. Rides the block's root, like the id. */
|
|
272
|
+
const scheduled = showScheduled && windowState(block, at) !== "live" ? windowState(block, at) : undefined;
|
|
273
|
+
const blockAttrs = style || inner || hide ? {} : nested ? { "data-bcms-window": scheduled } : { "data-bcms-block": block.id, "data-bcms-window": scheduled };
|
|
255
274
|
const node = (() => {
|
|
256
275
|
switch (block.type) {
|
|
257
276
|
case "heading": {
|
|
@@ -530,7 +549,7 @@ const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses
|
|
|
530
549
|
// to no field on one placement and collides into a dropped duplicate on two.
|
|
531
550
|
return (
|
|
532
551
|
<Astro.self
|
|
533
|
-
blocks={resolved.blocks}
|
|
552
|
+
blocks={showScheduled ? resolved.blocks : visibleBlocks(resolved.blocks, at)}
|
|
534
553
|
depth={depth + 1}
|
|
535
554
|
seen={[...seen, resolved.id]}
|
|
536
555
|
containerAttrs={{
|
|
@@ -566,9 +585,10 @@ const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses
|
|
|
566
585
|
* box is the root and carries the marker instead.
|
|
567
586
|
*/
|
|
568
587
|
const stamp = nested ? undefined : block.id;
|
|
569
|
-
if (style && inner) return <div style={style} data-bcms-block={stamp}><div class="bcms-block-inner" style={inner}>{node}</div></div>;
|
|
570
|
-
if (
|
|
571
|
-
if (
|
|
588
|
+
if (style && inner) return <div style={style} data-bcms-hide={hide} data-bcms-block={stamp} data-bcms-window={scheduled}><div class="bcms-block-inner" style={inner}>{node}</div></div>;
|
|
589
|
+
if (hide) return <div style={style} data-bcms-hide={hide} data-bcms-block={stamp} data-bcms-window={scheduled}>{inner ? <div class="bcms-block-inner" style={inner}>{node}</div> : node}</div>;
|
|
590
|
+
if (inner) return <div class="bcms-block-inner" style={inner} data-bcms-block={stamp} data-bcms-window={scheduled}>{node}</div>;
|
|
591
|
+
if (style) return <div style={style} data-bcms-block={stamp} data-bcms-window={scheduled}>{node}</div>;
|
|
572
592
|
return node;
|
|
573
593
|
})}
|
|
574
594
|
</div>
|
|
@@ -75,6 +75,72 @@ const countryList = () => {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Multi-step forms. A `step` field is a divider: the fields before the first one are page 1 and
|
|
81
|
+
* each divider opens the next. Every page is a <fieldset data-bcms-step>, so with no JavaScript
|
|
82
|
+
* the form is simply one long form; the inline script below shows one at a time. A form with no
|
|
83
|
+
* divider takes the single un-wrapped group, so its markup is exactly what it always was.
|
|
84
|
+
* The same rule lives in the hosted renderer and BcmsForm (next); each package publishes on its
|
|
85
|
+
* own and cannot count on a newer shared helper arriving with it.
|
|
86
|
+
*/
|
|
87
|
+
type Page = { label: string | null; key: string | null; fields: DeliveryFormField[] };
|
|
88
|
+
const all = form.fields ?? [];
|
|
89
|
+
let pages: Page[] | null = null;
|
|
90
|
+
if (all.some((f) => f.type === "step")) {
|
|
91
|
+
pages = [];
|
|
92
|
+
let cur: Page = { label: null, key: null, fields: [] };
|
|
93
|
+
for (const f of all) {
|
|
94
|
+
if (f.type !== "step") {
|
|
95
|
+
cur.fields.push(f);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (cur.fields.length) pages.push(cur);
|
|
99
|
+
cur = { label: f.label, key: f.key, fields: [] };
|
|
100
|
+
}
|
|
101
|
+
if (cur.fields.length) pages.push(cur);
|
|
102
|
+
}
|
|
103
|
+
const groups: Page[] = pages ?? [{ label: null, key: null, fields: all }];
|
|
104
|
+
const Wrap = pages ? "fieldset" : Fragment;
|
|
105
|
+
// Copy of FORM_STEPS_CSS in the hosted renderer: a <fieldset> otherwise draws its own border.
|
|
106
|
+
// Canonical copy is FORM_STEPS_JS in the backend's src/lib/sites/render-page.ts, pinned byte for
|
|
107
|
+
// byte by src/__tests__/content/form-script-pin.test.ts. Emitted only for a stepped form, ahead of
|
|
108
|
+
// the submit script, and idempotent per form, so several <BcmsForm>s on a page are fine.
|
|
109
|
+
const STEPS_JS = `document.querySelectorAll('form[data-bcms-steps]').forEach(function(f){
|
|
110
|
+
if(f.getAttribute('data-bcms-steps-on'))return;
|
|
111
|
+
f.setAttribute('data-bcms-steps-on','1');
|
|
112
|
+
var steps=[].slice.call(f.querySelectorAll('fieldset[data-bcms-step]'));
|
|
113
|
+
if(steps.length<2)return;
|
|
114
|
+
var last=steps.length-1,at=0,submit=f.querySelector('button[type="submit"]');
|
|
115
|
+
f.noValidate=true;
|
|
116
|
+
function bad(s){var c=s.querySelectorAll('input,select,textarea');for(var k=0;k<c.length;k++){if(!c[k].checkValidity())return c[k];}return null;}
|
|
117
|
+
function show(n,focus){
|
|
118
|
+
at=n;
|
|
119
|
+
steps.forEach(function(s,k){s.hidden=k!==n;});
|
|
120
|
+
if(submit)submit.hidden=n!==last;
|
|
121
|
+
var h=steps[n].querySelector('.bcms-step-title');
|
|
122
|
+
if(focus&&h)h.focus();
|
|
123
|
+
}
|
|
124
|
+
function next(){var c=bad(steps[at]);if(c){c.reportValidity();return;}show(at+1,true);}
|
|
125
|
+
steps.forEach(function(s,k){
|
|
126
|
+
var nav=document.createElement('div');nav.className='bcms-step-nav';
|
|
127
|
+
function btn(label,attr,fn){var b=document.createElement('button');b.type='button';b.setAttribute(attr,'');b.textContent=label;b.addEventListener('click',fn);nav.appendChild(b);}
|
|
128
|
+
if(k>0)btn('Back','data-bcms-step-back',function(){show(k-1,true);});
|
|
129
|
+
if(k<last)btn('Next','data-bcms-step-next',next);
|
|
130
|
+
s.appendChild(nav);
|
|
131
|
+
});
|
|
132
|
+
// Capture phase, so this runs ahead of the submit script however many copies of it are bound.
|
|
133
|
+
f.addEventListener('submit',function(e){
|
|
134
|
+
if(at<last){e.preventDefault();e.stopImmediatePropagation();next();return;}
|
|
135
|
+
for(var k=0;k<steps.length;k++){
|
|
136
|
+
var c=bad(steps[k]);
|
|
137
|
+
if(c){e.preventDefault();e.stopImmediatePropagation();show(k,false);c.reportValidity();return;}
|
|
138
|
+
}
|
|
139
|
+
},true);
|
|
140
|
+
f.addEventListener('reset',function(){show(0,false);});
|
|
141
|
+
show(0,false);
|
|
142
|
+
});`;
|
|
143
|
+
const STEPS_CSS = ".bcms-step{border:0;margin:0 0 14px;padding:0;min-width:0}.bcms-step-title{margin:0 0 12px;font-size:1.125em}.bcms-step-count{display:block;font-size:.8em;font-weight:500;color:var(--bcms-text-muted,#6b7280)}.bcms-step-nav{display:flex;gap:10px;margin-top:14px}.bcms-form .bcms-step-nav button[data-bcms-step-back]{background:transparent;color:inherit;border:1px solid #ccc}";
|
|
78
144
|
---
|
|
79
145
|
|
|
80
146
|
<form
|
|
@@ -82,11 +148,20 @@ const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
|
82
148
|
method="post"
|
|
83
149
|
action={action}
|
|
84
150
|
data-bcms-form={form.id}
|
|
151
|
+
data-bcms-steps={pages ? pages.length : undefined}
|
|
85
152
|
data-bcms-form-success={form.successMessage ?? "Thanks!"}
|
|
86
153
|
data-bcms-form-redirect={form.redirectUrl ?? ""}
|
|
87
154
|
{...rest}
|
|
88
155
|
>
|
|
89
|
-
{
|
|
156
|
+
{groups.map((page, pageIndex) => (
|
|
157
|
+
<Wrap {...(pages ? { class: "bcms-step", "data-bcms-step": pageIndex + 1, "aria-labelledby": `bcms-step-${form.id}-${pageIndex + 1}` } : {})}>
|
|
158
|
+
{pages && (
|
|
159
|
+
<h3 class="bcms-step-title" id={`bcms-step-${form.id}-${pageIndex + 1}`} tabindex="-1" data-bcms-form-field={page.key ?? undefined}>
|
|
160
|
+
<span class="bcms-step-count">Step {pageIndex + 1} of {pages.length}</span>
|
|
161
|
+
{page.label && <Fragment> <span data-bcms-form-part="label">{page.label}</span></Fragment>}
|
|
162
|
+
</h3>
|
|
163
|
+
)}
|
|
164
|
+
{page.fields.map((f) => {
|
|
90
165
|
const wrapAttrs = {
|
|
91
166
|
"data-bcms-form-field": f.key,
|
|
92
167
|
...(f.showIf ? { "data-bcms-showif": JSON.stringify(f.showIf) } : {}),
|
|
@@ -177,6 +252,8 @@ const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
|
177
252
|
</div>
|
|
178
253
|
);
|
|
179
254
|
})}
|
|
255
|
+
</Wrap>
|
|
256
|
+
))}
|
|
180
257
|
|
|
181
258
|
{form.honeypotField && (
|
|
182
259
|
<input type="text" name={form.honeypotField} tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true" />
|
|
@@ -188,8 +265,12 @@ const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
|
188
265
|
<p class="bcms-form-msg" hidden></p>
|
|
189
266
|
</form>
|
|
190
267
|
|
|
268
|
+
{pages && <style is:inline set:html={STEPS_CSS}></style>}
|
|
269
|
+
|
|
191
270
|
{showTurnstile && <script is:inline src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>}
|
|
192
271
|
|
|
272
|
+
{pages && <script is:inline set:html={STEPS_JS}></script>}
|
|
273
|
+
|
|
193
274
|
<script is:inline>
|
|
194
275
|
// Compose one E.164 value from a country-picker phone field. CANONICAL COPY is
|
|
195
276
|
// PHONE_COMPOSE_JS in the backend's src/lib/sites/render-page.ts, and this file is pinned
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AstroIntegration } from 'astro';
|
|
2
2
|
export { BcmsLocals, BetterCMSRuntime, RuntimeConfig, liveSrc } from './runtime.js';
|
|
3
3
|
import { BlockStyle, FieldStyles, FieldStyle } from '@bettercms-ai/types';
|
|
4
|
-
export { ContentBlock, DeliveredLayout, DeliveryEntry, DeliveryList, DeliveryPage, PageMetaJson, Perspective, SiteSeoDefaults } from '@bettercms-ai/types';
|
|
4
|
+
export { ContentBlock, DeliveredLayout, DeliveryEntry, DeliveryList, DeliveryPage, PageMetaJson, Perspective, RESPONSIVE_CSS, SiteSeoDefaults } from '@bettercms-ai/types';
|
|
5
5
|
export { ImageCrop, ImageFit, ImageFocal, ImageFormat, ImageSource, ImageUrlBuilder, ImageValueUrlOptions, imageUrl, default as imageUrlBuilder, imageValueObjectPosition, imageValueUrl } from '@bettercms-ai/image-url';
|
|
6
6
|
export { DeliveryForm, DeliveryFormField, ResolvedSeo, SeoInput, resolveSeo } from '@bettercms-ai/sdk';
|
|
7
7
|
|
|
@@ -82,6 +82,8 @@ interface SectionRootBlock {
|
|
|
82
82
|
interface SectionRootAttrs {
|
|
83
83
|
"data-bcms-block"?: string;
|
|
84
84
|
"data-bcms-style": "";
|
|
85
|
+
/** `"mobile tablet"` when `style.hideOn` hides it there; the site ships `RESPONSIVE_CSS` for it. */
|
|
86
|
+
"data-bcms-hide"?: string;
|
|
85
87
|
/** `blockStyleToCss(block.style)` as a CSS string; absent when the block has no style. */
|
|
86
88
|
style?: string;
|
|
87
89
|
}
|
package/dist/index.js
CHANGED
|
@@ -181,17 +181,22 @@ function liveSrc(opts) {
|
|
|
181
181
|
}
|
|
182
182
|
var enc = new TextEncoder();
|
|
183
183
|
|
|
184
|
+
// src/index.ts
|
|
185
|
+
import { RESPONSIVE_CSS } from "@bettercms-ai/types";
|
|
186
|
+
|
|
184
187
|
// src/section-root.ts
|
|
185
|
-
import { blockStyleToColumnCss, blockStyleToCss } from "@bettercms-ai/types";
|
|
188
|
+
import { blockStyleToColumnCss, blockStyleToCss, hideOnAttr } from "@bettercms-ai/types";
|
|
186
189
|
function cssText(css) {
|
|
187
190
|
return Object.entries(css).map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${v}`).join(";");
|
|
188
191
|
}
|
|
189
192
|
function sectionRootAttrs(block) {
|
|
190
193
|
const css = blockStyleToCss(block?.style ?? void 0);
|
|
191
194
|
const id = typeof block?.id === "string" && block.id !== "" ? block.id : void 0;
|
|
195
|
+
const hide = hideOnAttr(block?.style);
|
|
192
196
|
return {
|
|
193
197
|
...id !== void 0 ? { "data-bcms-block": id } : {},
|
|
194
198
|
"data-bcms-style": "",
|
|
199
|
+
...hide ? { "data-bcms-hide": hide } : {},
|
|
195
200
|
...css ? { style: cssText(css) } : {}
|
|
196
201
|
};
|
|
197
202
|
}
|
|
@@ -222,6 +227,7 @@ import {
|
|
|
222
227
|
import { resolveSeo } from "@bettercms-ai/sdk";
|
|
223
228
|
export {
|
|
224
229
|
DRAFT_COOKIE,
|
|
230
|
+
RESPONSIVE_CSS,
|
|
225
231
|
bettercms,
|
|
226
232
|
integration_default as default,
|
|
227
233
|
fieldAttrs,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts","../src/integration.ts","../src/runtime.ts","../src/section-root.ts","../src/field-attrs.ts","../src/index.ts"],"sourcesContent":["/**\n * Resolved configuration shared by the integration, runtime, middleware and\n * draft routes. Public values are safe to ship to the browser; the API key and\n * draft secret are read from server env at runtime and never live here.\n *\n * Resolution order is options → `PUBLIC_BCMS_*` env → the build snapshot. Config-time\n * only: this module is reached exclusively through `dist/index.js`, which only an\n * `astro.config.*` imports, so the `node:fs` read below never enters a client bundle.\n * Browser code gets the `bettercms:config` virtual module instead.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\n/**\n * The platform's canonical media host, matching `@bettercms-ai/image-url`'s own\n * `DEFAULT_BASE_URL` and the `CDN_URL` the backend mints asset URLs on.\n *\n * This used to be `media.bettercms.ai`. Both hostnames route to the backend — they are both in\n * `$bcms_api_host` in infra/nginx/backend-lb.conf — so nothing was broken, but a site built by\n * this integration disagreed with the URL the server itself hands out for the same asset. That\n * splits the CDN cache for identical bytes and makes an asset URL's origin depend on which code\n * path produced it, which is the exact drift image-url was moved off this host to end.\n *\n * A project that serves media from the API origin instead (`useCdn: false`) overrides this with\n * `PUBLIC_BCMS_MEDIA_URL` or the `mediaUrl` option.\n */\nconst DEFAULT_MEDIA_URL = \"https://cdn.bettercms.ai\";\nconst SNAPSHOT_FILE = \"bcms-content.json\";\nexport const DRAFT_COOKIE = \"bcms-draft\";\n\nexport interface BetterCMSAstroOptions {\n /** Backend base, e.g. `https://api.bettercms.ai`. Falls back to `PUBLIC_BCMS_API_URL`. */\n apiUrl?: string;\n /** Workspace slug. Falls back to `PUBLIC_BCMS_WORKSPACE`. */\n workspace?: string;\n /** Optional project scope for forms. Falls back to `PUBLIC_BCMS_PROJECT_ID`. */\n projectId?: string;\n /** Media host for the image-url builder. Defaults to the prod media host. */\n mediaUrl?: string;\n /** Dashboard base for visual-editing deep links. */\n studioUrl?: string;\n /** Enable the draft-mode middleware + injected draft routes. Default true. */\n draft?: boolean;\n}\n\n/** Browser-safe config — exposed via the `bettercms:config` virtual module. */\nexport interface PublicConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n mediaUrl: string;\n studioUrl?: string;\n draftCookie: string;\n}\n\nexport interface ResolvedConfig extends PublicConfig {\n draft: boolean;\n}\n\n/** The fields of the deploy Action's content snapshot that identify the site. */\ninterface Snapshot {\n apiUrl?: string;\n workspace?: string;\n projectId?: string;\n}\n\n/**\n * Read `bcms-content.json` — the snapshot the deploy Action writes next to the build,\n * BEFORE `astro build` runs — which already carries the workspace this site belongs to.\n *\n * This is what lets `bettercms()` work with no arguments and no env at all. Not reading it\n * was the whole bug: a hosted build had the answer sitting in its own working directory and\n * threw anyway, and every starter template had to reimplement this by hand to survive.\n *\n * Lazy: only consulted when options and env both miss, so the configured path does no I/O.\n * Absent/corrupt file ⇒ `{}`, and the caller raises the actionable error below.\n */\nlet snapshotCache: Snapshot | undefined;\nfunction snapshot(cwd: string): Snapshot {\n if (snapshotCache === undefined) {\n try {\n snapshotCache = JSON.parse(readFileSync(resolve(cwd, SNAPSHOT_FILE), \"utf8\")) as Snapshot;\n } catch {\n snapshotCache = {};\n }\n }\n return snapshotCache;\n}\n\n/** Test seam — the snapshot is memoised for the life of the process. */\nexport function resetSnapshotCache(): void {\n snapshotCache = undefined;\n}\n\nconst required = (name: string, envVar: string, extra: string) =>\n new Error(\n `[@bettercms-ai/astro] \\`${name}\\` is required. Pass \\`bettercms({ ${name} })\\`, set ` +\n `${envVar}, or ${extra}\\n` +\n `On BetterCMS hosting these are injected automatically — if this failed in a hosted ` +\n `build, the repository is running an outdated deploy workflow: publish once, or save ` +\n `Hosting settings, to refresh it.`,\n );\n\n/**\n * Merge integration options with `PUBLIC_BCMS_*` env and the build snapshot, validating the\n * essentials. Precedence: explicit options → env → snapshot.\n */\nexport function resolveConfig(\n options: BetterCMSAstroOptions,\n env: Record<string, string | undefined> = process.env,\n cwd: string = process.cwd(),\n): ResolvedConfig {\n const snap = (): Snapshot => snapshot(cwd);\n\n const apiUrl = (options.apiUrl ?? env.PUBLIC_BCMS_API_URL ?? snap().apiUrl ?? \"\").replace(/\\/+$/, \"\");\n const workspace = options.workspace ?? env.PUBLIC_BCMS_WORKSPACE ?? snap().workspace ?? \"\";\n // No default origin on purpose: guessing one produces a site that builds cleanly against\n // the wrong backend, which is far harder to notice than this error.\n if (!apiUrl) throw required(\"apiUrl\", \"PUBLIC_BCMS_API_URL\", `run the content fetch so ${SNAPSHOT_FILE} is present.`);\n if (!workspace) {\n throw required(\"workspace\", \"PUBLIC_BCMS_WORKSPACE\", `run the content fetch so ${SNAPSHOT_FILE} is present.`);\n }\n\n return {\n apiUrl,\n workspace,\n projectId: options.projectId ?? env.PUBLIC_BCMS_PROJECT_ID ?? snap().projectId,\n mediaUrl: (options.mediaUrl ?? env.PUBLIC_BCMS_MEDIA_URL ?? DEFAULT_MEDIA_URL).replace(/\\/+$/, \"\"),\n studioUrl: options.studioUrl ?? env.PUBLIC_BCMS_STUDIO_URL,\n draftCookie: DRAFT_COOKIE,\n draft: options.draft ?? true,\n };\n}\n\nexport function toPublicConfig(c: ResolvedConfig): PublicConfig {\n const { draft: _draft, ...pub } = c;\n return pub;\n}\n","/**\n * `bettercms()` — the Astro integration.\n *\n * - Registers two virtual modules:\n * `bettercms:client` → server runtime (client + typed loaders) — has the key.\n * `bettercms:config` → browser-safe public config — no secrets.\n * - Adds the draft-mode middleware and injects the draft enable/disable routes.\n */\nimport type { AstroIntegration } from \"astro\";\nimport {\n resolveConfig,\n toPublicConfig,\n type BetterCMSAstroOptions,\n type ResolvedConfig,\n} from \"./config.js\";\n\nconst CLIENT_ID = \"bettercms:client\";\nconst CONFIG_ID = \"bettercms:config\";\nconst RESOLVED = (id: string) => `\\0${id}`;\n\nfunction configModule(cfg: ResolvedConfig): string {\n return `export const config = ${JSON.stringify(toPublicConfig(cfg))};\\nexport default config;\\n`;\n}\n\nfunction clientModule(cfg: ResolvedConfig, buildCache: boolean): string {\n // The key is optional: published reads are addressed by the inlined projectId. When one is\n // set it is read from the server's env at run time (a server-output site gets it from its\n // host, not the build), falling back to the build's env; it never lands in a client bundle.\n return [\n `import { createRuntime } from \"@bettercms-ai/astro/runtime\";`,\n `const runtime = createRuntime({`,\n ` apiUrl: ${JSON.stringify(cfg.apiUrl)},`,\n ` workspace: ${JSON.stringify(cfg.workspace)},`,\n ` projectId: ${JSON.stringify(cfg.projectId)},`,\n ` apiKey: globalThis.process?.env?.BCMS_API_KEY ?? import.meta.env.BCMS_API_KEY,`,\n ` buildCache: ${JSON.stringify(buildCache)},`,\n `});`,\n `export const client = runtime.client;`,\n `export const getClient = runtime.getClient;`,\n `export const loadEntry = runtime.loadEntry;`,\n `export const loadEntries = runtime.loadEntries;`,\n `export const loadPage = runtime.loadPage;`,\n `export const loadComponents = runtime.loadComponents;`,\n `export const loadForms = runtime.loadForms;`,\n `export const loadLayout = runtime.loadLayout;`,\n `export default runtime;`,\n ].join(\"\\n\");\n}\n\nfunction virtualPlugin(cfg: ResolvedConfig, buildCache: () => boolean) {\n return {\n name: \"@bettercms-ai/astro:virtual\",\n resolveId(id: string) {\n if (id === CLIENT_ID || id === CONFIG_ID) return RESOLVED(id);\n return null;\n },\n load(id: string) {\n if (id === RESOLVED(CONFIG_ID)) return configModule(cfg);\n if (id === RESOLVED(CLIENT_ID)) return clientModule(cfg, buildCache());\n return null;\n },\n };\n}\n\nexport function bettercms(options: BetterCMSAstroOptions = {}): AstroIntegration {\n /**\n * Whether identical delivery GETs may be collapsed for the life of the process.\n *\n * Read lazily by the virtual module (see `astro:config:done` below) because only the\n * FINAL config settles `output` and `adapter` — another integration can add an adapter\n * after `astro:config:setup` runs.\n */\n const buildCache = { on: false, command: \"\", resolved: false };\n /** Set in `astro:config:done`, re-run in `astro:build:start` — see both hooks below. */\n let decide: (() => void) | undefined;\n\n return {\n name: \"@bettercms-ai/astro\",\n hooks: {\n \"astro:config:setup\": ({ command, updateConfig, addMiddleware, injectRoute, logger }) => {\n const cfg = resolveConfig(options);\n buildCache.command = command;\n\n updateConfig({ vite: { plugins: [virtualPlugin(cfg, () => buildCache.on)] } });\n\n if (cfg.draft) {\n addMiddleware({ entrypoint: \"@bettercms-ai/astro/middleware\", order: \"pre\" });\n injectRoute({\n pattern: \"/api/bcms/draft/enable\",\n entrypoint: \"@bettercms-ai/astro/draft-enable\",\n prerender: false,\n });\n injectRoute({\n pattern: \"/api/bcms/draft/disable\",\n entrypoint: \"@bettercms-ai/astro/draft-disable\",\n prerender: false,\n });\n logger.info(\"draft mode enabled — /api/bcms/draft/{enable,disable} injected\");\n }\n },\n\n /**\n * Decide whether the build-time request cache is safe, on the FINAL config.\n *\n * A fully static build renders every route from one published dataset, so identical\n * delivery GETs can collapse to a single round-trip. Without that a large site\n * re-fetched its nav/settings/collections once PER ROUTE and exhausted the delivery\n * rate limit mid-build — a 429 the build could not avoid.\n *\n * `command === \"build\"` is NOT sufficient to detect that, and getting this wrong is\n * far worse than the problem it solves: Astro also reports `command === \"build\"` when\n * it compiles `output: \"server\"`, whose bundle then runs as a LONG-LIVED process. A\n * module-lifetime cache there would pin the first response for every URL and serve it\n * to unrelated visitors until the process restarted — permanent stale content on a\n * production site.\n *\n * `buildOutput` (Astro 5) is Astro's OWN answer to \"will this emit a server?\", so it\n * is what we trust when present. Inferring it from `config.adapter` is not equivalent:\n * `setAdapter()` is documented as being called from inside `astro:config:done`, so an\n * adapter integration whose hook runs after this one would still read as absent here.\n *\n * On Astro 4 there is no `buildOutput`, and there `output: \"static\"` genuinely\n * prerenders everything — on-demand rendering required `\"server\"` or `\"hybrid\"`, both\n * of which fail this check. The adapter test stays as a second line of defence.\n */\n \"astro:config:done\": ({ config, logger, ...rest }) => {\n // Astro >= 5 only; undefined on 4, hence the fallback rather than a bare read.\n const buildOutput = (rest as { buildOutput?: \"static\" | \"server\" }).buildOutput;\n const staticOutput =\n buildOutput !== undefined\n ? buildOutput === \"static\"\n : config.output === \"static\" && !config.adapter;\n\n buildCache.on = buildCache.command === \"build\" && staticOutput;\n // Re-checked in astro:build:start below; only announce once it is final.\n decide = () => {\n const late = (rest as { buildOutput?: \"static\" | \"server\" }).buildOutput;\n const stillStatic =\n late !== undefined ? late === \"static\" : config.output === \"static\" && !config.adapter;\n buildCache.on = buildCache.command === \"build\" && stillStatic;\n buildCache.resolved = true;\n if (buildCache.on) {\n logger.info(\"static build — identical content reads will be fetched once\");\n }\n };\n },\n\n /**\n * Last word on the cache, and the reason this is not decided in `astro:config:done`\n * alone: Astro runs every integration's `config:done` in order, and `setAdapter()` is\n * called FROM that hook. An adapter registered by an integration ordered after this one\n * would flip the project to on-demand rendering *after* we had already recorded\n * \"static\" — and the stale `true` would ship a module-lifetime cache into a\n * long-lived server. `astro:build:start` runs after all of them and before Vite loads\n * the virtual module, so re-reading here is the value that is actually true.\n *\n * It never fires for `dev` or `preview`, which is exactly right: both must stay\n * uncached, and `buildCache.on` is only ever set to true under `command === \"build\"`.\n */\n \"astro:build:start\": () => {\n decide?.();\n },\n },\n };\n}\n\nexport default bettercms;\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// ── Signed-cookie helpers (HMAC-SHA256, base64url) ──────────────────────────\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage. */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** Verify a signed value, returning the original value or null if tampered. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n // Constant-time-ish compare via length + char accumulation.\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Only same-origin relative paths. Blocks \"//evil.com\", \"https://evil.com\", etc. */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") ? to : \"/\";\n}\n\n/** Decode a preview-token JWT payload (UNVERIFIED) to read its `entrySlug`. */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n const json = JSON.parse(\n atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n ) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n","/**\n * C3/C4 — `sectionRootAttrs(block)`: the attributes a site's OWN section root spreads so the\n * BetterCMS editor offers that section's spacing and style handles.\n *\n * <section class=\"hero\" {...sectionRootAttrs(block)}>…</section>\n *\n * The editor shows those handles on a site-rendered page only when EVERY root the site stamps\n * with `data-bcms-block` also carries `data-bcms-style`, which is the site's promise that \"this\n * root's inline style is `blockStyleToCss(block.style)`\" — the same declarations `<BcmsBlocks>`\n * puts on its own wrapper. A root that stamps the block without applying its style would get a\n * capsule that paints the canvas and never ships, so the marker must travel with the style.\n *\n * The marker is present even when the style is EMPTY (then there is no `style` attribute at all):\n * an unstyled section is exactly the one an author is about to give its first spacing edit.\n */\nimport { blockStyleToColumnCss, blockStyleToCss, type BlockStyle } from \"@bettercms-ai/types\";\n\nexport interface SectionRootBlock {\n id?: string | null;\n style?: BlockStyle | Record<string, unknown> | null;\n}\n\nexport interface SectionRootAttrs {\n \"data-bcms-block\"?: string;\n \"data-bcms-style\": \"\";\n /** `blockStyleToCss(block.style)` as a CSS string; absent when the block has no style. */\n style?: string;\n}\n\n/** `paddingTop` → `padding-top`, joined the way `<BcmsBlocks>` writes its wrapper's attribute. */\nfunction cssText(css: Record<string, string | number>): string {\n return Object.entries(css)\n .map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${v}`)\n .join(\";\");\n}\n\nexport function sectionRootAttrs(block: SectionRootBlock | null | undefined): SectionRootAttrs {\n const css = blockStyleToCss((block?.style ?? undefined) as BlockStyle | undefined);\n const id = typeof block?.id === \"string\" && block.id !== \"\" ? block.id : undefined;\n return {\n ...(id !== undefined ? { \"data-bcms-block\": id } : {}),\n \"data-bcms-style\": \"\",\n ...(css ? { style: cssText(css) } : {}),\n };\n}\n\nexport interface SectionColumnAttrs {\n \"data-bcms-column\": \"\";\n /** `blockStyleToColumnCss(block.style)` as a CSS string; absent when the section has no column. */\n style?: string;\n}\n\n/**\n * C3 — `sectionColumnAttrs(block)`: the attributes a site's own INNER container spreads so the\n * section's content WIDTH (`style.contentWidth`, placed by `contentAlign`) ships.\n *\n * <section {...sectionRootAttrs(block)}>\n * <div class=\"container\" {...sectionColumnAttrs(block)}>…</div>\n * </section>\n *\n * The width is the column BetterCMS draws as `.bcms-block-inner` inside the section's styled box —\n * never on the root itself, whose background must stay full-bleed. `data-bcms-column` is the\n * site's promise that this box's inline style is the column; the editor offers the width handle\n * on a site-rendered page only where every styled root holds one. Max-width plus left/right\n * margins, so the container keeps its own vertical margins and, with no column, its own width.\n */\nexport function sectionColumnAttrs(block: SectionRootBlock | null | undefined): SectionColumnAttrs {\n const css = blockStyleToColumnCss((block?.style ?? undefined) as BlockStyle | undefined);\n return { \"data-bcms-column\": \"\", ...(css ? { style: cssText(css) } : {}) };\n}\n","/**\n * C3 — `fieldAttrs(path, fieldStyles)`: the attributes a site's OWN element spreads to bind a PAGE\n * FIELD and apply that field's stored style, so the BetterCMS editor offers its width and spacing\n * handles and its Align / Size / Weight controls.\n *\n * ---\n * const page = await loadPage(Astro, \"home\");\n * ---\n * <h1 class=\"title\" {...fieldAttrs(\"hero-title\", page?.fieldStyles)}>{entry.data[\"hero-title\"]}</h1>\n *\n * `data-bcms-style` is the site's promise that \"this element's inline style is\n * `fieldStyleToCss(fieldStyles[path])`\". The editor offers a field's handles only on an element that\n * carries it: a handle on one that does not would paint the canvas and never ship. The marker is\n * present even when the field has no style (then there is no `style` attribute at all, byte for\n * byte the element it was): an unstyled field is the one an author is about to give its first edit.\n * Twin: `fieldAttrs` in @bettercms-ai/next (a React style object there).\n */\nimport { fieldStyleCssText, type FieldStyle, type FieldStyles } from \"@bettercms-ai/types\";\n\nexport interface FieldAttrs {\n \"data-bcms-field\": string;\n \"data-bcms-style\": \"\";\n /** `fieldStyleToCss(fieldStyles[path])` as a CSS string; absent when the field has no style. */\n style?: string;\n}\n\n/** The stored style for `path`, by OWN key only — a path like `constructor` never reads the prototype. */\nexport function fieldStyleOf(path: string, fieldStyles: FieldStyles | Record<string, unknown> | null | undefined): FieldStyle | undefined {\n if (!fieldStyles || typeof fieldStyles !== \"object\" || !Object.prototype.hasOwnProperty.call(fieldStyles, path)) return undefined;\n const style = (fieldStyles as Record<string, unknown>)[path];\n return style && typeof style === \"object\" ? (style as FieldStyle) : undefined;\n}\n\nexport function fieldAttrs(path: string, fieldStyles?: FieldStyles | Record<string, unknown> | null): FieldAttrs {\n const style = fieldStyleCssText(fieldStyleOf(path, fieldStyles));\n return { \"data-bcms-field\": path, \"data-bcms-style\": \"\", ...(style ? { style } : {}) };\n}\n","/**\n * @bettercms-ai/astro — the BetterCMS adapter for Astro.\n *\n * import { defineConfig } from \"astro/config\";\n * import bettercms from \"@bettercms-ai/astro\";\n *\n * export default defineConfig({\n * integrations: [bettercms({ apiUrl, workspace })],\n * output: \"server\",\n * });\n *\n * Then in any page: `import { loadPage } from \"bettercms:client\"`.\n */\nexport { bettercms, default } from \"./integration.js\";\nexport {\n resolveConfig,\n DRAFT_COOKIE,\n type BetterCMSAstroOptions,\n type PublicConfig,\n type ResolvedConfig,\n} from \"./config.js\";\nexport { liveSrc } from \"./runtime.js\";\n// C3/C4: a site's own section root declares that it paints block.style.\nexport { sectionRootAttrs, type SectionRootAttrs, type SectionRootBlock } from \"./section-root.js\";\nexport { fieldAttrs, fieldStyleOf, type FieldAttrs } from \"./field-attrs.js\";\nexport { sectionColumnAttrs, type SectionColumnAttrs } from \"./section-root.js\";\nexport type { BcmsLocals, BetterCMSRuntime, RuntimeConfig } from \"./runtime.js\";\n\n// Image URL builder, re-exported for `<BcmsImage>` and host code.\nexport {\n default as imageUrlBuilder,\n imageUrl,\n imageValueUrl,\n imageValueObjectPosition,\n} from \"@bettercms-ai/image-url\";\nexport type {\n ImageUrlBuilder,\n ImageSource,\n ImageFormat,\n ImageFit,\n ImageCrop,\n ImageFocal,\n ImageValueUrlOptions,\n} from \"@bettercms-ai/image-url\";\n\n// Delivery read types, re-exported for typing page/entry data.\nexport type {\n Perspective,\n DeliveryEntry,\n DeliveryPage,\n DeliveryList,\n PageMetaJson,\n SiteSeoDefaults,\n ContentBlock,\n DeliveredLayout,\n} from \"@bettercms-ai/types\";\nexport type { DeliveryForm, DeliveryFormField } from \"@bettercms-ai/sdk\";\n\n// SEO (FLO-302): page-over-site meta resolver. In a page's frontmatter,\n// `const seo = resolveSeo(page, siteDefaults)`, then map `seo.*` into `<head>`.\nexport { resolveSeo } from \"@bettercms-ai/sdk\";\nexport type { SeoInput, ResolvedSeo } from \"@bettercms-ai/sdk\";\n"],"mappings":"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","names":["default"]}
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/integration.ts","../src/runtime.ts","../src/index.ts","../src/section-root.ts","../src/field-attrs.ts"],"sourcesContent":["/**\n * Resolved configuration shared by the integration, runtime, middleware and\n * draft routes. Public values are safe to ship to the browser; the API key and\n * draft secret are read from server env at runtime and never live here.\n *\n * Resolution order is options → `PUBLIC_BCMS_*` env → the build snapshot. Config-time\n * only: this module is reached exclusively through `dist/index.js`, which only an\n * `astro.config.*` imports, so the `node:fs` read below never enters a client bundle.\n * Browser code gets the `bettercms:config` virtual module instead.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\n/**\n * The platform's canonical media host, matching `@bettercms-ai/image-url`'s own\n * `DEFAULT_BASE_URL` and the `CDN_URL` the backend mints asset URLs on.\n *\n * This used to be `media.bettercms.ai`. Both hostnames route to the backend — they are both in\n * `$bcms_api_host` in infra/nginx/backend-lb.conf — so nothing was broken, but a site built by\n * this integration disagreed with the URL the server itself hands out for the same asset. That\n * splits the CDN cache for identical bytes and makes an asset URL's origin depend on which code\n * path produced it, which is the exact drift image-url was moved off this host to end.\n *\n * A project that serves media from the API origin instead (`useCdn: false`) overrides this with\n * `PUBLIC_BCMS_MEDIA_URL` or the `mediaUrl` option.\n */\nconst DEFAULT_MEDIA_URL = \"https://cdn.bettercms.ai\";\nconst SNAPSHOT_FILE = \"bcms-content.json\";\nexport const DRAFT_COOKIE = \"bcms-draft\";\n\nexport interface BetterCMSAstroOptions {\n /** Backend base, e.g. `https://api.bettercms.ai`. Falls back to `PUBLIC_BCMS_API_URL`. */\n apiUrl?: string;\n /** Workspace slug. Falls back to `PUBLIC_BCMS_WORKSPACE`. */\n workspace?: string;\n /** Optional project scope for forms. Falls back to `PUBLIC_BCMS_PROJECT_ID`. */\n projectId?: string;\n /** Media host for the image-url builder. Defaults to the prod media host. */\n mediaUrl?: string;\n /** Dashboard base for visual-editing deep links. */\n studioUrl?: string;\n /** Enable the draft-mode middleware + injected draft routes. Default true. */\n draft?: boolean;\n}\n\n/** Browser-safe config — exposed via the `bettercms:config` virtual module. */\nexport interface PublicConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n mediaUrl: string;\n studioUrl?: string;\n draftCookie: string;\n}\n\nexport interface ResolvedConfig extends PublicConfig {\n draft: boolean;\n}\n\n/** The fields of the deploy Action's content snapshot that identify the site. */\ninterface Snapshot {\n apiUrl?: string;\n workspace?: string;\n projectId?: string;\n}\n\n/**\n * Read `bcms-content.json` — the snapshot the deploy Action writes next to the build,\n * BEFORE `astro build` runs — which already carries the workspace this site belongs to.\n *\n * This is what lets `bettercms()` work with no arguments and no env at all. Not reading it\n * was the whole bug: a hosted build had the answer sitting in its own working directory and\n * threw anyway, and every starter template had to reimplement this by hand to survive.\n *\n * Lazy: only consulted when options and env both miss, so the configured path does no I/O.\n * Absent/corrupt file ⇒ `{}`, and the caller raises the actionable error below.\n */\nlet snapshotCache: Snapshot | undefined;\nfunction snapshot(cwd: string): Snapshot {\n if (snapshotCache === undefined) {\n try {\n snapshotCache = JSON.parse(readFileSync(resolve(cwd, SNAPSHOT_FILE), \"utf8\")) as Snapshot;\n } catch {\n snapshotCache = {};\n }\n }\n return snapshotCache;\n}\n\n/** Test seam — the snapshot is memoised for the life of the process. */\nexport function resetSnapshotCache(): void {\n snapshotCache = undefined;\n}\n\nconst required = (name: string, envVar: string, extra: string) =>\n new Error(\n `[@bettercms-ai/astro] \\`${name}\\` is required. Pass \\`bettercms({ ${name} })\\`, set ` +\n `${envVar}, or ${extra}\\n` +\n `On BetterCMS hosting these are injected automatically — if this failed in a hosted ` +\n `build, the repository is running an outdated deploy workflow: publish once, or save ` +\n `Hosting settings, to refresh it.`,\n );\n\n/**\n * Merge integration options with `PUBLIC_BCMS_*` env and the build snapshot, validating the\n * essentials. Precedence: explicit options → env → snapshot.\n */\nexport function resolveConfig(\n options: BetterCMSAstroOptions,\n env: Record<string, string | undefined> = process.env,\n cwd: string = process.cwd(),\n): ResolvedConfig {\n const snap = (): Snapshot => snapshot(cwd);\n\n const apiUrl = (options.apiUrl ?? env.PUBLIC_BCMS_API_URL ?? snap().apiUrl ?? \"\").replace(/\\/+$/, \"\");\n const workspace = options.workspace ?? env.PUBLIC_BCMS_WORKSPACE ?? snap().workspace ?? \"\";\n // No default origin on purpose: guessing one produces a site that builds cleanly against\n // the wrong backend, which is far harder to notice than this error.\n if (!apiUrl) throw required(\"apiUrl\", \"PUBLIC_BCMS_API_URL\", `run the content fetch so ${SNAPSHOT_FILE} is present.`);\n if (!workspace) {\n throw required(\"workspace\", \"PUBLIC_BCMS_WORKSPACE\", `run the content fetch so ${SNAPSHOT_FILE} is present.`);\n }\n\n return {\n apiUrl,\n workspace,\n projectId: options.projectId ?? env.PUBLIC_BCMS_PROJECT_ID ?? snap().projectId,\n mediaUrl: (options.mediaUrl ?? env.PUBLIC_BCMS_MEDIA_URL ?? DEFAULT_MEDIA_URL).replace(/\\/+$/, \"\"),\n studioUrl: options.studioUrl ?? env.PUBLIC_BCMS_STUDIO_URL,\n draftCookie: DRAFT_COOKIE,\n draft: options.draft ?? true,\n };\n}\n\nexport function toPublicConfig(c: ResolvedConfig): PublicConfig {\n const { draft: _draft, ...pub } = c;\n return pub;\n}\n","/**\n * `bettercms()` — the Astro integration.\n *\n * - Registers two virtual modules:\n * `bettercms:client` → server runtime (client + typed loaders) — has the key.\n * `bettercms:config` → browser-safe public config — no secrets.\n * - Adds the draft-mode middleware and injects the draft enable/disable routes.\n */\nimport type { AstroIntegration } from \"astro\";\nimport {\n resolveConfig,\n toPublicConfig,\n type BetterCMSAstroOptions,\n type ResolvedConfig,\n} from \"./config.js\";\n\nconst CLIENT_ID = \"bettercms:client\";\nconst CONFIG_ID = \"bettercms:config\";\nconst RESOLVED = (id: string) => `\\0${id}`;\n\nfunction configModule(cfg: ResolvedConfig): string {\n return `export const config = ${JSON.stringify(toPublicConfig(cfg))};\\nexport default config;\\n`;\n}\n\nfunction clientModule(cfg: ResolvedConfig, buildCache: boolean): string {\n // The key is optional: published reads are addressed by the inlined projectId. When one is\n // set it is read from the server's env at run time (a server-output site gets it from its\n // host, not the build), falling back to the build's env; it never lands in a client bundle.\n return [\n `import { createRuntime } from \"@bettercms-ai/astro/runtime\";`,\n `const runtime = createRuntime({`,\n ` apiUrl: ${JSON.stringify(cfg.apiUrl)},`,\n ` workspace: ${JSON.stringify(cfg.workspace)},`,\n ` projectId: ${JSON.stringify(cfg.projectId)},`,\n ` apiKey: globalThis.process?.env?.BCMS_API_KEY ?? import.meta.env.BCMS_API_KEY,`,\n ` buildCache: ${JSON.stringify(buildCache)},`,\n `});`,\n `export const client = runtime.client;`,\n `export const getClient = runtime.getClient;`,\n `export const loadEntry = runtime.loadEntry;`,\n `export const loadEntries = runtime.loadEntries;`,\n `export const loadPage = runtime.loadPage;`,\n `export const loadComponents = runtime.loadComponents;`,\n `export const loadForms = runtime.loadForms;`,\n `export const loadLayout = runtime.loadLayout;`,\n `export default runtime;`,\n ].join(\"\\n\");\n}\n\nfunction virtualPlugin(cfg: ResolvedConfig, buildCache: () => boolean) {\n return {\n name: \"@bettercms-ai/astro:virtual\",\n resolveId(id: string) {\n if (id === CLIENT_ID || id === CONFIG_ID) return RESOLVED(id);\n return null;\n },\n load(id: string) {\n if (id === RESOLVED(CONFIG_ID)) return configModule(cfg);\n if (id === RESOLVED(CLIENT_ID)) return clientModule(cfg, buildCache());\n return null;\n },\n };\n}\n\nexport function bettercms(options: BetterCMSAstroOptions = {}): AstroIntegration {\n /**\n * Whether identical delivery GETs may be collapsed for the life of the process.\n *\n * Read lazily by the virtual module (see `astro:config:done` below) because only the\n * FINAL config settles `output` and `adapter` — another integration can add an adapter\n * after `astro:config:setup` runs.\n */\n const buildCache = { on: false, command: \"\", resolved: false };\n /** Set in `astro:config:done`, re-run in `astro:build:start` — see both hooks below. */\n let decide: (() => void) | undefined;\n\n return {\n name: \"@bettercms-ai/astro\",\n hooks: {\n \"astro:config:setup\": ({ command, updateConfig, addMiddleware, injectRoute, logger }) => {\n const cfg = resolveConfig(options);\n buildCache.command = command;\n\n updateConfig({ vite: { plugins: [virtualPlugin(cfg, () => buildCache.on)] } });\n\n if (cfg.draft) {\n addMiddleware({ entrypoint: \"@bettercms-ai/astro/middleware\", order: \"pre\" });\n injectRoute({\n pattern: \"/api/bcms/draft/enable\",\n entrypoint: \"@bettercms-ai/astro/draft-enable\",\n prerender: false,\n });\n injectRoute({\n pattern: \"/api/bcms/draft/disable\",\n entrypoint: \"@bettercms-ai/astro/draft-disable\",\n prerender: false,\n });\n logger.info(\"draft mode enabled — /api/bcms/draft/{enable,disable} injected\");\n }\n },\n\n /**\n * Decide whether the build-time request cache is safe, on the FINAL config.\n *\n * A fully static build renders every route from one published dataset, so identical\n * delivery GETs can collapse to a single round-trip. Without that a large site\n * re-fetched its nav/settings/collections once PER ROUTE and exhausted the delivery\n * rate limit mid-build — a 429 the build could not avoid.\n *\n * `command === \"build\"` is NOT sufficient to detect that, and getting this wrong is\n * far worse than the problem it solves: Astro also reports `command === \"build\"` when\n * it compiles `output: \"server\"`, whose bundle then runs as a LONG-LIVED process. A\n * module-lifetime cache there would pin the first response for every URL and serve it\n * to unrelated visitors until the process restarted — permanent stale content on a\n * production site.\n *\n * `buildOutput` (Astro 5) is Astro's OWN answer to \"will this emit a server?\", so it\n * is what we trust when present. Inferring it from `config.adapter` is not equivalent:\n * `setAdapter()` is documented as being called from inside `astro:config:done`, so an\n * adapter integration whose hook runs after this one would still read as absent here.\n *\n * On Astro 4 there is no `buildOutput`, and there `output: \"static\"` genuinely\n * prerenders everything — on-demand rendering required `\"server\"` or `\"hybrid\"`, both\n * of which fail this check. The adapter test stays as a second line of defence.\n */\n \"astro:config:done\": ({ config, logger, ...rest }) => {\n // Astro >= 5 only; undefined on 4, hence the fallback rather than a bare read.\n const buildOutput = (rest as { buildOutput?: \"static\" | \"server\" }).buildOutput;\n const staticOutput =\n buildOutput !== undefined\n ? buildOutput === \"static\"\n : config.output === \"static\" && !config.adapter;\n\n buildCache.on = buildCache.command === \"build\" && staticOutput;\n // Re-checked in astro:build:start below; only announce once it is final.\n decide = () => {\n const late = (rest as { buildOutput?: \"static\" | \"server\" }).buildOutput;\n const stillStatic =\n late !== undefined ? late === \"static\" : config.output === \"static\" && !config.adapter;\n buildCache.on = buildCache.command === \"build\" && stillStatic;\n buildCache.resolved = true;\n if (buildCache.on) {\n logger.info(\"static build — identical content reads will be fetched once\");\n }\n };\n },\n\n /**\n * Last word on the cache, and the reason this is not decided in `astro:config:done`\n * alone: Astro runs every integration's `config:done` in order, and `setAdapter()` is\n * called FROM that hook. An adapter registered by an integration ordered after this one\n * would flip the project to on-demand rendering *after* we had already recorded\n * \"static\" — and the stale `true` would ship a module-lifetime cache into a\n * long-lived server. `astro:build:start` runs after all of them and before Vite loads\n * the virtual module, so re-reading here is the value that is actually true.\n *\n * It never fires for `dev` or `preview`, which is exactly right: both must stay\n * uncached, and `buildCache.on` is only ever set to true under `command === \"build\"`.\n */\n \"astro:build:start\": () => {\n decide?.();\n },\n },\n };\n}\n\nexport default bettercms;\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// ── Signed-cookie helpers (HMAC-SHA256, base64url) ──────────────────────────\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage. */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** Verify a signed value, returning the original value or null if tampered. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n // Constant-time-ish compare via length + char accumulation.\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Only same-origin relative paths. Blocks \"//evil.com\", \"https://evil.com\", etc. */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") ? to : \"/\";\n}\n\n/** Decode a preview-token JWT payload (UNVERIFIED) to read its `entrySlug`. */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n const json = JSON.parse(\n atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n ) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n","/**\n * @bettercms-ai/astro — the BetterCMS adapter for Astro.\n *\n * import { defineConfig } from \"astro/config\";\n * import bettercms from \"@bettercms-ai/astro\";\n *\n * export default defineConfig({\n * integrations: [bettercms({ apiUrl, workspace })],\n * output: \"server\",\n * });\n *\n * Then in any page: `import { loadPage } from \"bettercms:client\"`.\n */\nexport { bettercms, default } from \"./integration.js\";\nexport {\n resolveConfig,\n DRAFT_COOKIE,\n type BetterCMSAstroOptions,\n type PublicConfig,\n type ResolvedConfig,\n} from \"./config.js\";\nexport { liveSrc } from \"./runtime.js\";\n// C3/C4: a site's own section root declares that it paints block.style.\nexport { RESPONSIVE_CSS } from \"@bettercms-ai/types\";\nexport { sectionRootAttrs, type SectionRootAttrs, type SectionRootBlock } from \"./section-root.js\";\nexport { fieldAttrs, fieldStyleOf, type FieldAttrs } from \"./field-attrs.js\";\nexport { sectionColumnAttrs, type SectionColumnAttrs } from \"./section-root.js\";\nexport type { BcmsLocals, BetterCMSRuntime, RuntimeConfig } from \"./runtime.js\";\n\n// Image URL builder, re-exported for `<BcmsImage>` and host code.\nexport {\n default as imageUrlBuilder,\n imageUrl,\n imageValueUrl,\n imageValueObjectPosition,\n} from \"@bettercms-ai/image-url\";\nexport type {\n ImageUrlBuilder,\n ImageSource,\n ImageFormat,\n ImageFit,\n ImageCrop,\n ImageFocal,\n ImageValueUrlOptions,\n} from \"@bettercms-ai/image-url\";\n\n// Delivery read types, re-exported for typing page/entry data.\nexport type {\n Perspective,\n DeliveryEntry,\n DeliveryPage,\n DeliveryList,\n PageMetaJson,\n SiteSeoDefaults,\n ContentBlock,\n DeliveredLayout,\n} from \"@bettercms-ai/types\";\nexport type { DeliveryForm, DeliveryFormField } from \"@bettercms-ai/sdk\";\n\n// SEO (FLO-302): page-over-site meta resolver. In a page's frontmatter,\n// `const seo = resolveSeo(page, siteDefaults)`, then map `seo.*` into `<head>`.\nexport { resolveSeo } from \"@bettercms-ai/sdk\";\nexport type { SeoInput, ResolvedSeo } from \"@bettercms-ai/sdk\";\n","/**\n * C3/C4 — `sectionRootAttrs(block)`: the attributes a site's OWN section root spreads so the\n * BetterCMS editor offers that section's spacing and style handles.\n *\n * <section class=\"hero\" {...sectionRootAttrs(block)}>…</section>\n *\n * The editor shows those handles on a site-rendered page only when EVERY root the site stamps\n * with `data-bcms-block` also carries `data-bcms-style`, which is the site's promise that \"this\n * root's inline style is `blockStyleToCss(block.style)`\" — the same declarations `<BcmsBlocks>`\n * puts on its own wrapper. A root that stamps the block without applying its style would get a\n * capsule that paints the canvas and never ships, so the marker must travel with the style.\n *\n * The marker is present even when the style is EMPTY (then there is no `style` attribute at all):\n * an unstyled section is exactly the one an author is about to give its first spacing edit.\n */\nimport { blockStyleToColumnCss, blockStyleToCss, hideOnAttr, type BlockStyle } from \"@bettercms-ai/types\";\n\nexport interface SectionRootBlock {\n id?: string | null;\n style?: BlockStyle | Record<string, unknown> | null;\n}\n\nexport interface SectionRootAttrs {\n \"data-bcms-block\"?: string;\n \"data-bcms-style\": \"\";\n /** `\"mobile tablet\"` when `style.hideOn` hides it there; the site ships `RESPONSIVE_CSS` for it. */\n \"data-bcms-hide\"?: string;\n /** `blockStyleToCss(block.style)` as a CSS string; absent when the block has no style. */\n style?: string;\n}\n\n/** `paddingTop` → `padding-top`, joined the way `<BcmsBlocks>` writes its wrapper's attribute. */\nfunction cssText(css: Record<string, string | number>): string {\n return Object.entries(css)\n .map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${v}`)\n .join(\";\");\n}\n\nexport function sectionRootAttrs(block: SectionRootBlock | null | undefined): SectionRootAttrs {\n const css = blockStyleToCss((block?.style ?? undefined) as BlockStyle | undefined);\n const id = typeof block?.id === \"string\" && block.id !== \"\" ? block.id : undefined;\n const hide = hideOnAttr(block?.style);\n return {\n ...(id !== undefined ? { \"data-bcms-block\": id } : {}),\n \"data-bcms-style\": \"\",\n ...(hide ? { \"data-bcms-hide\": hide } : {}),\n ...(css ? { style: cssText(css) } : {}),\n };\n}\n\nexport interface SectionColumnAttrs {\n \"data-bcms-column\": \"\";\n /** `blockStyleToColumnCss(block.style)` as a CSS string; absent when the section has no column. */\n style?: string;\n}\n\n/**\n * C3 — `sectionColumnAttrs(block)`: the attributes a site's own INNER container spreads so the\n * section's content WIDTH (`style.contentWidth`, placed by `contentAlign`) ships.\n *\n * <section {...sectionRootAttrs(block)}>\n * <div class=\"container\" {...sectionColumnAttrs(block)}>…</div>\n * </section>\n *\n * The width is the column BetterCMS draws as `.bcms-block-inner` inside the section's styled box —\n * never on the root itself, whose background must stay full-bleed. `data-bcms-column` is the\n * site's promise that this box's inline style is the column; the editor offers the width handle\n * on a site-rendered page only where every styled root holds one. Max-width plus left/right\n * margins, so the container keeps its own vertical margins and, with no column, its own width.\n */\nexport function sectionColumnAttrs(block: SectionRootBlock | null | undefined): SectionColumnAttrs {\n const css = blockStyleToColumnCss((block?.style ?? undefined) as BlockStyle | undefined);\n return { \"data-bcms-column\": \"\", ...(css ? { style: cssText(css) } : {}) };\n}\n","/**\n * C3 — `fieldAttrs(path, fieldStyles)`: the attributes a site's OWN element spreads to bind a PAGE\n * FIELD and apply that field's stored style, so the BetterCMS editor offers its width and spacing\n * handles and its Align / Size / Weight controls.\n *\n * ---\n * const page = await loadPage(Astro, \"home\");\n * ---\n * <h1 class=\"title\" {...fieldAttrs(\"hero-title\", page?.fieldStyles)}>{entry.data[\"hero-title\"]}</h1>\n *\n * `data-bcms-style` is the site's promise that \"this element's inline style is\n * `fieldStyleToCss(fieldStyles[path])`\". The editor offers a field's handles only on an element that\n * carries it: a handle on one that does not would paint the canvas and never ship. The marker is\n * present even when the field has no style (then there is no `style` attribute at all, byte for\n * byte the element it was): an unstyled field is the one an author is about to give its first edit.\n * Twin: `fieldAttrs` in @bettercms-ai/next (a React style object there).\n */\nimport { fieldStyleCssText, type FieldStyle, type FieldStyles } from \"@bettercms-ai/types\";\n\nexport interface FieldAttrs {\n \"data-bcms-field\": string;\n \"data-bcms-style\": \"\";\n /** `fieldStyleToCss(fieldStyles[path])` as a CSS string; absent when the field has no style. */\n style?: string;\n}\n\n/** The stored style for `path`, by OWN key only — a path like `constructor` never reads the prototype. */\nexport function fieldStyleOf(path: string, fieldStyles: FieldStyles | Record<string, unknown> | null | undefined): FieldStyle | undefined {\n if (!fieldStyles || typeof fieldStyles !== \"object\" || !Object.prototype.hasOwnProperty.call(fieldStyles, path)) return undefined;\n const style = (fieldStyles as Record<string, unknown>)[path];\n return style && typeof style === \"object\" ? (style as FieldStyle) : undefined;\n}\n\nexport function fieldAttrs(path: string, fieldStyles?: FieldStyles | Record<string, unknown> | null): FieldAttrs {\n const style = fieldStyleCssText(fieldStyleOf(path, fieldStyles));\n return { \"data-bcms-field\": path, \"data-bcms-style\": \"\", ...(style ? { style } : {}) };\n}\n"],"mappings":"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","names":["default"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettercms-ai/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The BetterCMS adapter for Astro — a `bettercms()` integration, a `bettercms:client` virtual module, typed content loaders, draft preview, and native .astro rendering components.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,11 +55,11 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@bettercms-ai/image-url": "^0.4.0",
|
|
58
|
-
"@bettercms-ai/next": "^0.
|
|
58
|
+
"@bettercms-ai/next": "^0.22.0",
|
|
59
59
|
"@bettercms-ai/richtext": "^0.8.1",
|
|
60
|
-
"@bettercms-ai/sdk": "^1.
|
|
61
|
-
"@bettercms-ai/types": "^1.
|
|
62
|
-
"@bettercms-ai/ui": "^0.
|
|
60
|
+
"@bettercms-ai/sdk": "^1.28.0",
|
|
61
|
+
"@bettercms-ai/types": "^1.39.0",
|
|
62
|
+
"@bettercms-ai/ui": "^0.15.0",
|
|
63
63
|
"libphonenumber-js": "^1.13.13"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|