@bettercms-ai/astro 0.25.0 → 0.27.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 +32 -0
- package/components/BcmsBlocks.astro +14 -0
- package/components/BcmsButton.astro +16 -4
- package/components/BcmsForm.astro +1 -1
- package/components/BcmsImage.astro +1 -1
- package/components/BcmsSections.astro +88 -0
- package/dist/bindings.d.ts +3 -1
- package/dist/bindings.js +2 -1
- package/dist/bindings.js.map +1 -1
- package/dist/config-CU7bp5dd.d.ts +68 -0
- package/dist/index.d.ts +5 -95
- package/dist/index.js +48 -11
- package/dist/index.js.map +1 -1
- package/dist/public-config.d.ts +1 -0
- package/dist/public-config.js +42 -0
- package/dist/public-config.js.map +1 -0
- package/dist/section-root-JrXFxiBm.d.ts +53 -0
- package/dist/sections.d.ts +65 -0
- package/dist/sections.js +181 -0
- package/dist/sections.js.map +1 -0
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -151,6 +151,38 @@ typed — the value is still composed correctly on submit. `@bettercms-ai/next`
|
|
|
151
151
|
choice and ships a formatter in a lazy chunk.
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
## Sections in the CMS order: `<BcmsSections>`
|
|
155
|
+
|
|
156
|
+
A page whose sections are written in its `.astro` file renders them in the order its CMS page
|
|
157
|
+
lists them. Each section goes in a named slot, named by its placement's bind; the markup inside is
|
|
158
|
+
yours and is rendered as written.
|
|
159
|
+
|
|
160
|
+
```astro
|
|
161
|
+
---
|
|
162
|
+
import BcmsSections from "@bettercms-ai/astro/components/BcmsSections.astro";
|
|
163
|
+
---
|
|
164
|
+
<Layout>
|
|
165
|
+
<BcmsSections page="home">
|
|
166
|
+
<Fragment slot="hero"><Hero /></Fragment>
|
|
167
|
+
<Fragment slot="code:sec1"><LogoStrip /></Fragment>
|
|
168
|
+
<Fragment slot="features"><Features /></Fragment>
|
|
169
|
+
</BcmsSections>
|
|
170
|
+
</Layout>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- A block bound to a slot renders that slot at the block's position. Its root element gets
|
|
174
|
+
`data-bcms-block`, `data-bcms-style` and the block's style; no element is added around it.
|
|
175
|
+
- A section added in the editor (a block with no bind) is drawn by `<BcmsBlocks>`, which
|
|
176
|
+
`<BcmsSections>` imports itself. It needs no `bettercms()` on the build: without the integration
|
|
177
|
+
the components read `PUBLIC_BCMS_*` and the content snapshot. `Blocks` swaps in another renderer.
|
|
178
|
+
- A slot no block names: `code:<bind>` always renders where you wrote it. A bare `<bind>` does too
|
|
179
|
+
until the list names one of the page's slots; after that it is a removed section.
|
|
180
|
+
- With no snapshot (a local build, the API down) every slot renders in source order, untouched.
|
|
181
|
+
|
|
182
|
+
The block list is the published snapshot's (`bcms-content.json`), or the draft's when the Visual
|
|
183
|
+
Editor frames a server-rendered page in draft mode. Pass `blocks` to give it yourself.
|
|
184
|
+
`npx @bettercms-ai/convert --sections` writes this wrap for you.
|
|
185
|
+
|
|
154
186
|
## Live editing on a static site — `<BcmsLiveBlocks>`
|
|
155
187
|
|
|
156
188
|
When the Visual Editor frames a static build it can only show what was built, so editing a
|
|
@@ -352,6 +352,20 @@ const pass = { forms, components, turnstileSiteKey, nested: true, fieldAddresses
|
|
|
352
352
|
);
|
|
353
353
|
}
|
|
354
354
|
case "button":
|
|
355
|
+
// The site's script owns the click: a `<button type="button">`, never an anchor with an invented href.
|
|
356
|
+
if (p.action === "code") return (
|
|
357
|
+
<button
|
|
358
|
+
type="button"
|
|
359
|
+
class={`bcms-btn bcms-btn-${buttonVariantOf(p.variant)}${p.size === "sm" || p.size === "lg" ? ` bcms-btn-${p.size}` : ""}${p.fullWidth === true ? " bcms-btn-full" : ""}`}
|
|
360
|
+
style={buttonPaintAttr(block)}
|
|
361
|
+
{...bind(block.id, "text", "text", bctx)}
|
|
362
|
+
{...blockAttrs}
|
|
363
|
+
>
|
|
364
|
+
{buttonIconOf(p.icon) && <Fragment set:html={buttonIconSvg(buttonIconOf(p.icon)!, "bcms-btn-icon")} />}
|
|
365
|
+
{p.text}
|
|
366
|
+
{p.arrow === true && <Fragment set:html={buttonIconSvg("arrow-right", "bcms-btn-arrow")} />}
|
|
367
|
+
</button>
|
|
368
|
+
);
|
|
355
369
|
return (
|
|
356
370
|
<a
|
|
357
371
|
class={`bcms-btn bcms-btn-${buttonVariantOf(p.variant)}${p.size === "sm" || p.size === "lg" ? ` bcms-btn-${p.size}` : ""}${p.fullWidth === true ? " bcms-btn-full" : ""}`}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* <BcmsButton> — render a `button` field ({ label, href, variant?, size?, arrow?, icon?,
|
|
4
|
-
* newTab? }) as one editable button, the way the BetterCMS visual editor and in-place publishing
|
|
4
|
+
* newTab?, fullWidth? }) as one editable button, the way the BetterCMS visual editor and in-place publishing
|
|
5
5
|
* expect to find it:
|
|
6
6
|
*
|
|
7
|
-
* <a data-bcms-button="<path>" data-variant data-size href [target rel] data-bcms-props="<path>.href|url|href">
|
|
7
|
+
* <a data-bcms-button="<path>" data-variant data-size [data-full-width] href [target rel] data-bcms-props="<path>.href|url|href">
|
|
8
8
|
* [<svg class="bcms-btn-icon">]<span data-bcms-field="<path>.label">Label</span>[<svg class="bcms-btn-arrow">]
|
|
9
9
|
* </a>
|
|
10
10
|
*
|
|
11
|
+
* A value with `action: "code"` (no href: the site's script handles the click) renders the same
|
|
12
|
+
* children in `<button type="button" data-bcms-button="<path>" data-variant data-size>`.
|
|
13
|
+
*
|
|
11
14
|
* Style it by `data-variant` (primary · secondary · tertiary · brand · dark · light · outline ·
|
|
12
15
|
* link) and `data-size` (sm · md · lg) in your own CSS: the editor repaints those attributes as the
|
|
13
16
|
* author picks, and publishing writes them into the live page, so your stylesheet decides what a
|
|
14
|
-
* look looks like.
|
|
17
|
+
* look looks like. A full-width button also carries `display:flex;width:100%;justify-content:center`
|
|
18
|
+
* inline, so it stretches across its column with no rule of yours. `class` is passed through untouched.
|
|
15
19
|
*
|
|
16
20
|
* <BcmsButton path="hero.cta" value={page.hero.cta} class="btn" />
|
|
17
21
|
*/
|
|
@@ -27,13 +31,21 @@ interface Props {
|
|
|
27
31
|
const { path, value, class: className } = Astro.props;
|
|
28
32
|
const b = readButtonValue(value);
|
|
29
33
|
---
|
|
30
|
-
{b.label && (
|
|
34
|
+
{b.label && b.action === "code" && (
|
|
35
|
+
<button type="button" class={className} data-bcms-button={path} data-variant={b.variant} data-size={b.size} {...(b.fullWidth ? { "data-full-width": "", style: "display:flex;width:100%;justify-content:center" } : {})}>
|
|
36
|
+
{b.icon && <Fragment set:html={buttonIconSvg(b.icon, "bcms-btn-icon")} />}
|
|
37
|
+
<span data-bcms-field={`${path}.label`} data-bcms-kind="text">{b.label}</span>
|
|
38
|
+
{b.arrow && <Fragment set:html={buttonIconSvg("arrow-right", "bcms-btn-arrow")} />}
|
|
39
|
+
</button>
|
|
40
|
+
)}
|
|
41
|
+
{b.label && b.action !== "code" && (
|
|
31
42
|
<a
|
|
32
43
|
class={className}
|
|
33
44
|
href={b.href || "#"}
|
|
34
45
|
data-bcms-button={path}
|
|
35
46
|
data-variant={b.variant}
|
|
36
47
|
data-size={b.size}
|
|
48
|
+
{...(b.fullWidth ? { "data-full-width": "", style: "display:flex;width:100%;justify-content:center" } : {})}
|
|
37
49
|
data-bcms-props={formatPropsAttribute([{ address: `${path}.href`, kind: "url", attr: "href" }])}
|
|
38
50
|
{...(b.newTab ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
39
51
|
>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Markup is class-driven and unstyled — the host owns CSS (matches the
|
|
8
8
|
* server-side renderer's class names: bcms-form, bcms-field, bcms-check, …).
|
|
9
9
|
*/
|
|
10
|
-
import config from "bettercms
|
|
10
|
+
import config from "@bettercms-ai/astro/public-config";
|
|
11
11
|
import type { DeliveryForm, DeliveryFormField } from "@bettercms-ai/sdk";
|
|
12
12
|
import { getCountries, getCountryCallingCode, isSupportedCountry, getExampleNumber } from "libphonenumber-js/min";
|
|
13
13
|
import phoneExamples from "libphonenumber-js/mobile/examples";
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* is what `sizes` selects from. The value's `altText` is the alt unless `alt` says otherwise.
|
|
16
16
|
* The same markup as @bettercms-ai/next's <BcmsImage>.
|
|
17
17
|
*/
|
|
18
|
-
import config from "bettercms
|
|
18
|
+
import config from "@bettercms-ai/astro/public-config";
|
|
19
19
|
import imageUrlBuilder, {
|
|
20
20
|
focalInCrop,
|
|
21
21
|
normalizeImageCrop,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <BcmsSections> — a code-built page's sections, in the order its CMS page lists them.
|
|
4
|
+
*
|
|
5
|
+
* <BcmsSections page="home" Blocks={BcmsBlocks}>
|
|
6
|
+
* <Fragment slot="hero"><Hero title="…" /></Fragment>
|
|
7
|
+
* <Fragment slot="features"><Features /></Fragment>
|
|
8
|
+
* </BcmsSections>
|
|
9
|
+
*
|
|
10
|
+
* Each section the page can draw is a named slot, named by its placement's bind: the markup inside
|
|
11
|
+
* is the site's own and is rendered as written. The CMS page's block list decides the rest:
|
|
12
|
+
*
|
|
13
|
+
* - a block bound to a slot renders that slot at the block's position, with `data-bcms-block`, the
|
|
14
|
+
* `data-bcms-style` marker and the block's style on the section's own root (no wrapper);
|
|
15
|
+
* - a block with no bind (a section added from the library) is drawn by `<BcmsBlocks>`, with or
|
|
16
|
+
* without `bettercms()` registered on this build. `Blocks` swaps in another renderer;
|
|
17
|
+
* - a slot no block names: `code:<bind>` always renders where the source has it; a bare `<bind>`
|
|
18
|
+
* does too until the list names one of this page's slots, and from then on it is a removed section.
|
|
19
|
+
*
|
|
20
|
+
* With no block list (a build without a snapshot, the API down) every slot renders in source order,
|
|
21
|
+
* untouched. Server-rendered only: no script, and no element of its own around the sections.
|
|
22
|
+
* Twin: `BcmsSections` in @bettercms-ai/next/sections; the order is `planSections` in @bettercms-ai/types.
|
|
23
|
+
*/
|
|
24
|
+
import type { ContentBlock } from "@bettercms-ai/types";
|
|
25
|
+
import { RESPONSIVE_CSS, planSections, usesHideOn, visibleBlocks } from "@bettercms-ai/types";
|
|
26
|
+
import { SECTIONS_END, draftSections, publishedSections, sectionsApiUrl, sectionsStart, stampSectionRoot } from "../dist/sections.js";
|
|
27
|
+
import BcmsBlocks from "./BcmsBlocks.astro";
|
|
28
|
+
|
|
29
|
+
interface Props {
|
|
30
|
+
/** The CMS page's slug: `home`, `services`. */
|
|
31
|
+
page: string;
|
|
32
|
+
/** A dynamic route (`[slug].astro`): `page` is the collection page, whose list is its item template's, drawn on every entry. */
|
|
33
|
+
template?: boolean;
|
|
34
|
+
/** The block list to follow instead of the snapshot's (or the draft's). `null` renders the source order. */
|
|
35
|
+
blocks?: ContentBlock[] | null;
|
|
36
|
+
/** What draws a block the page has no slot for. Default: `BcmsBlocks` from this package. */
|
|
37
|
+
Blocks?: any;
|
|
38
|
+
/** Component definitions and forms for `Blocks`. Default: the snapshot's. */
|
|
39
|
+
components?: unknown[];
|
|
40
|
+
forms?: unknown[];
|
|
41
|
+
/** The slots in source order, when it must be said outright. Default: the order they are written in. */
|
|
42
|
+
order?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { page, template = false, blocks: given, Blocks = BcmsBlocks, components, forms, order } = Astro.props;
|
|
46
|
+
const names = (order ?? Object.keys(Astro.slots)).filter((name) => name !== "default" && Astro.slots.has(name));
|
|
47
|
+
|
|
48
|
+
const published = given === undefined ? publishedSections(page, template) : null;
|
|
49
|
+
const bcms = (Astro.locals as { bcms?: { draft?: boolean; token?: string } } | undefined)?.bcms;
|
|
50
|
+
const draft = published && bcms?.draft && bcms.token ? await draftSections(page, bcms.token, sectionsApiUrl(published.apiUrl)) : null;
|
|
51
|
+
const list = given !== undefined ? given : (draft ?? published?.blocks ?? null);
|
|
52
|
+
const plan = planSections(names, Array.isArray(list) ? visibleBlocks(list, new Date()) : null);
|
|
53
|
+
|
|
54
|
+
// A section that throws is CONTAINED to itself: on a page rendered per request the response is
|
|
55
|
+
// already streaming, and one section's exception would end it there. Left out and logged in
|
|
56
|
+
// production; in dev it still fails loudly.
|
|
57
|
+
const renderSlot = async (name: string): Promise<string> => {
|
|
58
|
+
try {
|
|
59
|
+
return await Astro.slots.render(name);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (import.meta.env.DEV) throw error;
|
|
62
|
+
console.error(`[BcmsSections] section "${name}" failed to render on ${Astro.url.pathname}:`, error);
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Consecutive CMS blocks share one `Blocks` call, so its container wraps the run once.
|
|
68
|
+
const parts: Array<{ html: string } | { blocks: ContentBlock[] }> = [];
|
|
69
|
+
for (const item of plan) {
|
|
70
|
+
if (item.slot !== null) {
|
|
71
|
+
const html = await renderSlot(item.slot);
|
|
72
|
+
parts.push({ html: item.block ? stampSectionRoot(html, item.block, page) : html });
|
|
73
|
+
} else {
|
|
74
|
+
const last = parts[parts.length - 1];
|
|
75
|
+
if (last && "blocks" in last) last.blocks.push(item.block);
|
|
76
|
+
else parts.push({ blocks: [item.block] });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Only when a placed section hides somewhere, so a page without `hideOn` renders exactly as before.
|
|
80
|
+
const hides = usesHideOn(plan.filter((item) => item.slot !== null).map((item) => item.block));
|
|
81
|
+
---
|
|
82
|
+
<Fragment set:html={sectionsStart(page)} />{hides && <style is:inline set:html={RESPONSIVE_CSS}></style>}{parts.map((part) =>
|
|
83
|
+
"html" in part ? (
|
|
84
|
+
<Fragment set:html={part.html} />
|
|
85
|
+
) : (
|
|
86
|
+
<Blocks blocks={part.blocks} components={components ?? published?.components ?? []} forms={forms ?? published?.forms ?? []} />
|
|
87
|
+
),
|
|
88
|
+
)}<slot /><Fragment set:html={SECTIONS_END} />
|
package/dist/bindings.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ declare function bindProps(id: string | undefined, entries: readonly {
|
|
|
38
38
|
attr: string;
|
|
39
39
|
present: boolean;
|
|
40
40
|
}[], ctx: BindingCtx): Record<string, string>;
|
|
41
|
+
/** Where the tag opened at `from` (`<`) ends, quotes respected; -1 when it never does. */
|
|
42
|
+
declare function tagEnd(html: string, from: number): number;
|
|
41
43
|
/**
|
|
42
44
|
* `<BcmsField>`'s child, when it is ONE element: the same markup with the binding attributes on
|
|
43
45
|
* that element's own tag. Null when the rendered slot is anything else (text, several elements,
|
|
@@ -53,4 +55,4 @@ declare function bindProps(id: string | undefined, entries: readonly {
|
|
|
53
55
|
*/
|
|
54
56
|
declare function stampSoleElement(html: string, attrs: Record<string, string | undefined>): string | null;
|
|
55
57
|
|
|
56
|
-
export { type BindingCtx, addressFor, bind, bindProps, stampSoleElement };
|
|
58
|
+
export { type BindingCtx, addressFor, bind, bindProps, stampSoleElement, tagEnd };
|
package/dist/bindings.js
CHANGED
package/dist/bindings.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bindings.ts"],"sourcesContent":["/**\n * Visual Editor addresses for the Astro renderer.\n *\n * Identical contract to `@bettercms-ai/next`'s `bindings.ts`, and the SAME reason for existing:\n * inside a component instance the definition's block ids address nothing, because they are\n * shared by every instance of it. Stamping them produces an orphan on one placement and a\n * duplicate that drops BOTH on two.\n *\n * Kept as a `.ts` module rather than inlined in the `.astro` frontmatter so the logic is\n * testable without an Astro compiler, and so the two SDKs cannot drift from each other by\n * having one file edited and not the other.\n */\nimport type { BindingKind } from \"@bettercms-ai/types\";\nimport { formatPropsAttribute } from \"@bettercms-ai/types\";\n\nexport interface BindingCtx {\n /** Present only inside a component instance, where it is AUTHORITATIVE. */\n fieldAddresses?: Map<string, string>;\n}\n\n/** The address a write lands on, or null when none exists. */\nexport function addressFor(id: string | undefined, prop: string, ctx: BindingCtx): string | null {\n if (!id) return null;\n const key = `${id}__${prop}`;\n if (ctx.fieldAddresses) return ctx.fieldAddresses.get(key) ?? null;\n return key;\n}\n\n/**\n * Attributes for a value rendered as this element's own content. Astro omits `undefined`\n * attributes, so an empty object spreads to nothing.\n */\nexport function bind(\n id: string | undefined,\n prop: string,\n kind: BindingKind,\n ctx: BindingCtx,\n): Record<string, string> {\n const address = addressFor(id, prop, ctx);\n return address ? { \"data-bcms-field\": address, \"data-bcms-kind\": kind } : {};\n}\n\n/**\n * Attributes for values that live in an ATTRIBUTE of this element. `data-bcms-field` addresses\n * one element and an element cannot hold two, so these ride `data-bcms-props`:\n * `<address>|<kind>|<domAttribute>`, semicolon separated.\n *\n * An entry whose value is absent is dropped — `setFieldPath` refuses to create a final key, so\n * the address would be a write the editor rejects.\n */\nexport function bindProps(\n id: string | undefined,\n entries: readonly { prop: string; kind: BindingKind; attr: string; present: boolean }[],\n ctx: BindingCtx,\n): Record<string, string> {\n const resolved = entries.flatMap(({ prop, kind, attr, present }) => {\n if (!present) return [];\n const address = addressFor(id, prop, ctx);\n return address ? [{ address, kind, attr }] : [];\n });\n return resolved.length ? { \"data-bcms-props\": formatPropsAttribute(resolved) } : {};\n}\n\nconst VOID_TAGS = new Set([\"area\", \"base\", \"br\", \"col\", \"embed\", \"hr\", \"img\", \"input\", \"link\", \"meta\", \"source\", \"track\", \"wbr\"]);\nconst RAW_TEXT_TAGS = new Set([\"script\", \"style\", \"textarea\", \"title\"]);\n\n/** Where the tag opened at `from` (`<`) ends, quotes respected; -1 when it never does. */\
|
|
1
|
+
{"version":3,"sources":["../src/bindings.ts"],"sourcesContent":["/**\n * Visual Editor addresses for the Astro renderer.\n *\n * Identical contract to `@bettercms-ai/next`'s `bindings.ts`, and the SAME reason for existing:\n * inside a component instance the definition's block ids address nothing, because they are\n * shared by every instance of it. Stamping them produces an orphan on one placement and a\n * duplicate that drops BOTH on two.\n *\n * Kept as a `.ts` module rather than inlined in the `.astro` frontmatter so the logic is\n * testable without an Astro compiler, and so the two SDKs cannot drift from each other by\n * having one file edited and not the other.\n */\nimport type { BindingKind } from \"@bettercms-ai/types\";\nimport { formatPropsAttribute } from \"@bettercms-ai/types\";\n\nexport interface BindingCtx {\n /** Present only inside a component instance, where it is AUTHORITATIVE. */\n fieldAddresses?: Map<string, string>;\n}\n\n/** The address a write lands on, or null when none exists. */\nexport function addressFor(id: string | undefined, prop: string, ctx: BindingCtx): string | null {\n if (!id) return null;\n const key = `${id}__${prop}`;\n if (ctx.fieldAddresses) return ctx.fieldAddresses.get(key) ?? null;\n return key;\n}\n\n/**\n * Attributes for a value rendered as this element's own content. Astro omits `undefined`\n * attributes, so an empty object spreads to nothing.\n */\nexport function bind(\n id: string | undefined,\n prop: string,\n kind: BindingKind,\n ctx: BindingCtx,\n): Record<string, string> {\n const address = addressFor(id, prop, ctx);\n return address ? { \"data-bcms-field\": address, \"data-bcms-kind\": kind } : {};\n}\n\n/**\n * Attributes for values that live in an ATTRIBUTE of this element. `data-bcms-field` addresses\n * one element and an element cannot hold two, so these ride `data-bcms-props`:\n * `<address>|<kind>|<domAttribute>`, semicolon separated.\n *\n * An entry whose value is absent is dropped — `setFieldPath` refuses to create a final key, so\n * the address would be a write the editor rejects.\n */\nexport function bindProps(\n id: string | undefined,\n entries: readonly { prop: string; kind: BindingKind; attr: string; present: boolean }[],\n ctx: BindingCtx,\n): Record<string, string> {\n const resolved = entries.flatMap(({ prop, kind, attr, present }) => {\n if (!present) return [];\n const address = addressFor(id, prop, ctx);\n return address ? [{ address, kind, attr }] : [];\n });\n return resolved.length ? { \"data-bcms-props\": formatPropsAttribute(resolved) } : {};\n}\n\nconst VOID_TAGS = new Set([\"area\", \"base\", \"br\", \"col\", \"embed\", \"hr\", \"img\", \"input\", \"link\", \"meta\", \"source\", \"track\", \"wbr\"]);\nconst RAW_TEXT_TAGS = new Set([\"script\", \"style\", \"textarea\", \"title\"]);\n\n/** Where the tag opened at `from` (`<`) ends, quotes respected; -1 when it never does. */\nexport function tagEnd(html: string, from: number): number {\n let quote = \"\";\n for (let i = from; i < html.length; i++) {\n const c = html[i]!;\n if (quote) {\n if (c === quote) quote = \"\";\n } else if (c === '\"' || c === \"'\") quote = c;\n else if (c === \">\") return i;\n }\n return -1;\n}\n\n/**\n * `<BcmsField>`'s child, when it is ONE element: the same markup with the binding attributes on\n * that element's own tag. Null when the rendered slot is anything else (text, several elements,\n * an element that already declares a field, a framework island), and the caller wraps as before.\n *\n * Why not always wrap: the wrapper is a `<span>` between the author's parent and the author's\n * element. `.hero > h1`, `gsap.utils.toArray(\".hero > *\")` and `:first-child` all stop matching,\n * and an animation that targeted the heading targets nothing. With the attributes on the element\n * itself the page's DOM is exactly the one the author wrote.\n *\n * A scan, not a parser: it only has to find where the first element ends. Anything it is not\n * sure of answers null, which is the old behaviour.\n */\nexport function stampSoleElement(html: string, attrs: Record<string, string | undefined>): string | null {\n const start = html.search(/\\S/);\n const open = /^<([a-zA-Z][\\w:-]*)/.exec(html.slice(start));\n if (start < 0 || !open) return null;\n const tag = open[1]!.toLowerCase();\n const openEnd = tagEnd(html, start);\n // A custom element is a framework's own wrapper (`astro-island`): not the author's element.\n if (openEnd < 0 || tag.includes(\"-\") || /\\sdata-bcms-(?:layout-)?field[\\s=>/]/.test(html.slice(start, openEnd + 1))) return null;\n let i = openEnd + 1;\n if (!VOID_TAGS.has(tag) && html[openEnd - 1] !== \"/\") {\n let depth = 1;\n let raw = RAW_TEXT_TAGS.has(tag) ? tag : \"\";\n while (depth > 0) {\n const lt = html.indexOf(\"<\", i);\n if (lt < 0) return null;\n if (raw) {\n const close = html.toLowerCase().indexOf(`</${raw}`, i);\n if (close < 0) return null;\n i = tagEnd(html, close) + 1;\n if (i === 0) return null;\n raw = \"\";\n depth--;\n continue;\n }\n if (html.startsWith(\"<!--\", lt)) {\n const end = html.indexOf(\"-->\", lt + 4);\n if (end < 0) return null;\n i = end + 3;\n continue;\n }\n const end = tagEnd(html, lt);\n if (end < 0) return null;\n const name = /^<\\/?([a-zA-Z][\\w:-]*)/.exec(html.slice(lt, end + 1))?.[1]?.toLowerCase();\n if (name && html[lt + 1] === \"/\") depth--;\n else if (name && !VOID_TAGS.has(name) && html[end - 1] !== \"/\") {\n depth++;\n if (RAW_TEXT_TAGS.has(name)) raw = name;\n }\n i = end + 1;\n }\n }\n if (html.slice(i).trim() !== \"\") return null;\n const escape = (value: string) => value.replace(/&/g, \"&\").replace(/\"/g, \""\");\n const added = Object.entries(attrs)\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([name, value]) => ` ${name}=\"${escape(value)}\"`)\n .join(\"\");\n // Last on the tag, where React's `cloneElement` puts them: the two lanes emit the same bytes.\n const selfClosed = html[openEnd - 1] === \"/\";\n const at = selfClosed ? openEnd - 1 : openEnd;\n return html.slice(0, at).replace(/\\s+$/, \"\") + added + (selfClosed ? \" \" : \"\") + html.slice(at);\n}\n"],"mappings":"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","names":["end"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform's canonical media host, matching `@bettercms-ai/image-url`'s own
|
|
3
|
+
* `DEFAULT_BASE_URL` and the `CDN_URL` the backend mints asset URLs on.
|
|
4
|
+
*
|
|
5
|
+
* This used to be `media.bettercms.ai`. Both hostnames route to the backend — they are both in
|
|
6
|
+
* `$bcms_api_host` in infra/nginx/backend-lb.conf — so nothing was broken, but a site built by
|
|
7
|
+
* this integration disagreed with the URL the server itself hands out for the same asset. That
|
|
8
|
+
* splits the CDN cache for identical bytes and makes an asset URL's origin depend on which code
|
|
9
|
+
* path produced it, which is the exact drift image-url was moved off this host to end.
|
|
10
|
+
*
|
|
11
|
+
* A project that serves media from the API origin instead (`useCdn: false`) overrides this with
|
|
12
|
+
* `PUBLIC_BCMS_MEDIA_URL` or the `mediaUrl` option.
|
|
13
|
+
*/
|
|
14
|
+
declare const DEFAULT_MEDIA_URL = "https://cdn.bettercms.ai";
|
|
15
|
+
declare const DRAFT_COOKIE = "bcms-draft";
|
|
16
|
+
/** The specifier the components import, and the integration's plugin answers. */
|
|
17
|
+
declare const PUBLIC_CONFIG_ID = "@bettercms-ai/astro/public-config";
|
|
18
|
+
/**
|
|
19
|
+
* The config with no integration: `PUBLIC_BCMS_*`, then the snapshot. Never throws: with neither,
|
|
20
|
+
* `apiUrl` and `workspace` are empty, which a build that never talks to the API renders with.
|
|
21
|
+
*/
|
|
22
|
+
declare function fallbackConfig(): PublicConfig;
|
|
23
|
+
declare const config: PublicConfig;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolved configuration shared by the integration, runtime, middleware and
|
|
27
|
+
* draft routes. Public values are safe to ship to the browser; the API key and
|
|
28
|
+
* draft secret are read from server env at runtime and never live here.
|
|
29
|
+
*
|
|
30
|
+
* Resolution order is options → `PUBLIC_BCMS_*` env → the build snapshot. Config-time
|
|
31
|
+
* only: this module is reached exclusively through `dist/index.js`, which only an
|
|
32
|
+
* `astro.config.*` imports, so the `node:fs` read below never enters a client bundle.
|
|
33
|
+
* Browser code gets the `bettercms:config` virtual module instead.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
interface BetterCMSAstroOptions {
|
|
37
|
+
/** Backend base, e.g. `https://api.bettercms.ai`. Falls back to `PUBLIC_BCMS_API_URL`. */
|
|
38
|
+
apiUrl?: string;
|
|
39
|
+
/** Workspace slug. Falls back to `PUBLIC_BCMS_WORKSPACE`. */
|
|
40
|
+
workspace?: string;
|
|
41
|
+
/** Optional project scope for forms. Falls back to `PUBLIC_BCMS_PROJECT_ID`. */
|
|
42
|
+
projectId?: string;
|
|
43
|
+
/** Media host for the image-url builder. Defaults to the prod media host. */
|
|
44
|
+
mediaUrl?: string;
|
|
45
|
+
/** Dashboard base for visual-editing deep links. */
|
|
46
|
+
studioUrl?: string;
|
|
47
|
+
/** Enable the draft-mode middleware + injected draft routes. Default true. */
|
|
48
|
+
draft?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Browser-safe config — exposed via the `bettercms:config` virtual module. */
|
|
51
|
+
interface PublicConfig {
|
|
52
|
+
apiUrl: string;
|
|
53
|
+
workspace: string;
|
|
54
|
+
projectId?: string;
|
|
55
|
+
mediaUrl: string;
|
|
56
|
+
studioUrl?: string;
|
|
57
|
+
draftCookie: string;
|
|
58
|
+
}
|
|
59
|
+
interface ResolvedConfig extends PublicConfig {
|
|
60
|
+
draft: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Merge integration options with `PUBLIC_BCMS_*` env and the build snapshot, validating the
|
|
64
|
+
* essentials. Precedence: explicit options → env → snapshot.
|
|
65
|
+
*/
|
|
66
|
+
declare function resolveConfig(options: BetterCMSAstroOptions, env?: Record<string, string | undefined>, cwd?: string): ResolvedConfig;
|
|
67
|
+
|
|
68
|
+
export { type BetterCMSAstroOptions as B, DRAFT_COOKIE as D, type PublicConfig as P, type ResolvedConfig as R, DEFAULT_MEDIA_URL as a, PUBLIC_CONFIG_ID as b, config as c, fallbackConfig as f, resolveConfig as r };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,53 +1,13 @@
|
|
|
1
1
|
import { AstroIntegration } from 'astro';
|
|
2
|
+
import { B as BetterCMSAstroOptions } from './config-CU7bp5dd.js';
|
|
3
|
+
export { D as DRAFT_COOKIE, P as PublicConfig, R as ResolvedConfig, r as resolveConfig } from './config-CU7bp5dd.js';
|
|
2
4
|
export { BcmsLocals, BetterCMSRuntime, RuntimeConfig, liveSrc } from './runtime.js';
|
|
3
|
-
import {
|
|
5
|
+
import { FieldStyles, FieldStyle } from '@bettercms-ai/types';
|
|
4
6
|
export { ContentBlock, DeliveredLayout, DeliveryEntry, DeliveryList, DeliveryPage, LinkValue, PageMetaJson, Perspective, RESPONSIVE_CSS, SiteSeoDefaults, linkHref } from '@bettercms-ai/types';
|
|
7
|
+
export { S as SectionColumnAttrs, a as SectionRootAttrs, b as SectionRootBlock, s as sectionColumnAttrs, c as sectionRootAttrs } from './section-root-JrXFxiBm.js';
|
|
5
8
|
export { ImageCrop, ImageFit, ImageFocal, ImageFormat, ImageSource, ImageUrlBuilder, ImageValueUrlOptions, imageUrl, default as imageUrlBuilder, imageValueObjectPosition, imageValueUrl } from '@bettercms-ai/image-url';
|
|
6
9
|
export { DeliveryForm, DeliveryFormField, ResolvedSeo, SeoInput, resolveSeo } from '@bettercms-ai/sdk';
|
|
7
10
|
|
|
8
|
-
/**
|
|
9
|
-
* Resolved configuration shared by the integration, runtime, middleware and
|
|
10
|
-
* draft routes. Public values are safe to ship to the browser; the API key and
|
|
11
|
-
* draft secret are read from server env at runtime and never live here.
|
|
12
|
-
*
|
|
13
|
-
* Resolution order is options → `PUBLIC_BCMS_*` env → the build snapshot. Config-time
|
|
14
|
-
* only: this module is reached exclusively through `dist/index.js`, which only an
|
|
15
|
-
* `astro.config.*` imports, so the `node:fs` read below never enters a client bundle.
|
|
16
|
-
* Browser code gets the `bettercms:config` virtual module instead.
|
|
17
|
-
*/
|
|
18
|
-
declare const DRAFT_COOKIE = "bcms-draft";
|
|
19
|
-
interface BetterCMSAstroOptions {
|
|
20
|
-
/** Backend base, e.g. `https://api.bettercms.ai`. Falls back to `PUBLIC_BCMS_API_URL`. */
|
|
21
|
-
apiUrl?: string;
|
|
22
|
-
/** Workspace slug. Falls back to `PUBLIC_BCMS_WORKSPACE`. */
|
|
23
|
-
workspace?: string;
|
|
24
|
-
/** Optional project scope for forms. Falls back to `PUBLIC_BCMS_PROJECT_ID`. */
|
|
25
|
-
projectId?: string;
|
|
26
|
-
/** Media host for the image-url builder. Defaults to the prod media host. */
|
|
27
|
-
mediaUrl?: string;
|
|
28
|
-
/** Dashboard base for visual-editing deep links. */
|
|
29
|
-
studioUrl?: string;
|
|
30
|
-
/** Enable the draft-mode middleware + injected draft routes. Default true. */
|
|
31
|
-
draft?: boolean;
|
|
32
|
-
}
|
|
33
|
-
/** Browser-safe config — exposed via the `bettercms:config` virtual module. */
|
|
34
|
-
interface PublicConfig {
|
|
35
|
-
apiUrl: string;
|
|
36
|
-
workspace: string;
|
|
37
|
-
projectId?: string;
|
|
38
|
-
mediaUrl: string;
|
|
39
|
-
studioUrl?: string;
|
|
40
|
-
draftCookie: string;
|
|
41
|
-
}
|
|
42
|
-
interface ResolvedConfig extends PublicConfig {
|
|
43
|
-
draft: boolean;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Merge integration options with `PUBLIC_BCMS_*` env and the build snapshot, validating the
|
|
47
|
-
* essentials. Precedence: explicit options → env → snapshot.
|
|
48
|
-
*/
|
|
49
|
-
declare function resolveConfig(options: BetterCMSAstroOptions, env?: Record<string, string | undefined>, cwd?: string): ResolvedConfig;
|
|
50
|
-
|
|
51
11
|
/**
|
|
52
12
|
* `bettercms()` — the Astro integration.
|
|
53
13
|
*
|
|
@@ -59,56 +19,6 @@ declare function resolveConfig(options: BetterCMSAstroOptions, env?: Record<stri
|
|
|
59
19
|
|
|
60
20
|
declare function bettercms(options?: BetterCMSAstroOptions): AstroIntegration;
|
|
61
21
|
|
|
62
|
-
/**
|
|
63
|
-
* C3/C4 — `sectionRootAttrs(block)`: the attributes a site's OWN section root spreads so the
|
|
64
|
-
* BetterCMS editor offers that section's spacing and style handles.
|
|
65
|
-
*
|
|
66
|
-
* <section class="hero" {...sectionRootAttrs(block)}>…</section>
|
|
67
|
-
*
|
|
68
|
-
* The editor shows those handles on a site-rendered page only when EVERY root the site stamps
|
|
69
|
-
* with `data-bcms-block` also carries `data-bcms-style`, which is the site's promise that "this
|
|
70
|
-
* root's inline style is `blockStyleToCss(block.style)`" — the same declarations `<BcmsBlocks>`
|
|
71
|
-
* puts on its own wrapper. A root that stamps the block without applying its style would get a
|
|
72
|
-
* capsule that paints the canvas and never ships, so the marker must travel with the style.
|
|
73
|
-
*
|
|
74
|
-
* The marker is present even when the style is EMPTY (then there is no `style` attribute at all):
|
|
75
|
-
* an unstyled section is exactly the one an author is about to give its first spacing edit.
|
|
76
|
-
*/
|
|
77
|
-
|
|
78
|
-
interface SectionRootBlock {
|
|
79
|
-
id?: string | null;
|
|
80
|
-
style?: BlockStyle | Record<string, unknown> | null;
|
|
81
|
-
}
|
|
82
|
-
interface SectionRootAttrs {
|
|
83
|
-
"data-bcms-block"?: string;
|
|
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;
|
|
87
|
-
/** `blockStyleToCss(block.style)` as a CSS string; absent when the block has no style. */
|
|
88
|
-
style?: string;
|
|
89
|
-
}
|
|
90
|
-
declare function sectionRootAttrs(block: SectionRootBlock | null | undefined): SectionRootAttrs;
|
|
91
|
-
interface SectionColumnAttrs {
|
|
92
|
-
"data-bcms-column": "";
|
|
93
|
-
/** `blockStyleToColumnCss(block.style)` as a CSS string; absent when the section has no column. */
|
|
94
|
-
style?: string;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* C3 — `sectionColumnAttrs(block)`: the attributes a site's own INNER container spreads so the
|
|
98
|
-
* section's content WIDTH (`style.contentWidth`, placed by `contentAlign`) ships.
|
|
99
|
-
*
|
|
100
|
-
* <section {...sectionRootAttrs(block)}>
|
|
101
|
-
* <div class="container" {...sectionColumnAttrs(block)}>…</div>
|
|
102
|
-
* </section>
|
|
103
|
-
*
|
|
104
|
-
* The width is the column BetterCMS draws as `.bcms-block-inner` inside the section's styled box —
|
|
105
|
-
* never on the root itself, whose background must stay full-bleed. `data-bcms-column` is the
|
|
106
|
-
* site's promise that this box's inline style is the column; the editor offers the width handle
|
|
107
|
-
* on a site-rendered page only where every styled root holds one. Max-width plus left/right
|
|
108
|
-
* margins, so the container keeps its own vertical margins and, with no column, its own width.
|
|
109
|
-
*/
|
|
110
|
-
declare function sectionColumnAttrs(block: SectionRootBlock | null | undefined): SectionColumnAttrs;
|
|
111
|
-
|
|
112
22
|
/**
|
|
113
23
|
* C3 — `fieldAttrs(path, fieldStyles)`: the attributes a site's OWN element spreads to bind a PAGE
|
|
114
24
|
* FIELD and apply that field's stored style, so the BetterCMS editor offers its width and spacing
|
|
@@ -138,4 +48,4 @@ interface FieldAttrs {
|
|
|
138
48
|
declare function fieldStyleOf(path: string, fieldStyles: FieldStyles | Record<string, unknown> | null | undefined): FieldStyle | undefined;
|
|
139
49
|
declare function fieldAttrs(path: string, fieldStyles?: FieldStyles | Record<string, unknown> | null): FieldAttrs;
|
|
140
50
|
|
|
141
|
-
export {
|
|
51
|
+
export { BetterCMSAstroOptions, type FieldAttrs, bettercms, bettercms as default, fieldAttrs, fieldStyleOf };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { resolve } from "path";
|
|
4
|
+
|
|
5
|
+
// src/public-config.ts
|
|
6
|
+
import { readPublishedContent } from "@bettercms-ai/sdk";
|
|
4
7
|
var DEFAULT_MEDIA_URL = "https://cdn.bettercms.ai";
|
|
5
|
-
var SNAPSHOT_FILE = "bcms-content.json";
|
|
6
8
|
var DRAFT_COOKIE = "bcms-draft";
|
|
9
|
+
var PUBLIC_CONFIG_ID = "@bettercms-ai/astro/public-config";
|
|
10
|
+
function env(name) {
|
|
11
|
+
const fromProcess = typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
12
|
+
if (fromProcess) return fromProcess;
|
|
13
|
+
try {
|
|
14
|
+
return import.meta.env?.[name] || void 0;
|
|
15
|
+
} catch {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
var trim = (url) => url.replace(/\/+$/, "");
|
|
20
|
+
var text = (value) => typeof value === "string" && value ? value : void 0;
|
|
21
|
+
function fallbackConfig() {
|
|
22
|
+
const snapshot2 = readPublishedContent() ?? {};
|
|
23
|
+
return {
|
|
24
|
+
apiUrl: trim(env("PUBLIC_BCMS_API_URL") ?? text(snapshot2.apiUrl) ?? ""),
|
|
25
|
+
workspace: env("PUBLIC_BCMS_WORKSPACE") ?? text(snapshot2.workspace) ?? "",
|
|
26
|
+
projectId: env("PUBLIC_BCMS_PROJECT_ID") ?? text(snapshot2.projectId),
|
|
27
|
+
mediaUrl: trim(env("PUBLIC_BCMS_MEDIA_URL") ?? DEFAULT_MEDIA_URL),
|
|
28
|
+
studioUrl: env("PUBLIC_BCMS_STUDIO_URL"),
|
|
29
|
+
draftCookie: DRAFT_COOKIE
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
var KEYS = ["apiUrl", "workspace", "projectId", "mediaUrl", "studioUrl", "draftCookie"];
|
|
33
|
+
var config = Object.defineProperties(
|
|
34
|
+
{},
|
|
35
|
+
Object.fromEntries(KEYS.map((key) => [key, { enumerable: true, get: () => fallbackConfig()[key] }]))
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// src/config.ts
|
|
39
|
+
var SNAPSHOT_FILE = "bcms-content.json";
|
|
7
40
|
var snapshotCache;
|
|
8
41
|
function snapshot(cwd) {
|
|
9
42
|
if (snapshotCache === void 0) {
|
|
@@ -19,10 +52,10 @@ var required = (name, envVar, extra) => new Error(
|
|
|
19
52
|
`[@bettercms-ai/astro] \`${name}\` is required. Pass \`bettercms({ ${name} })\`, set ${envVar}, or ${extra}
|
|
20
53
|
On BetterCMS hosting these are injected automatically \u2014 if this failed in a hosted build, the repository is running an outdated deploy workflow: publish once, or save Hosting settings, to refresh it.`
|
|
21
54
|
);
|
|
22
|
-
function resolveConfig(options,
|
|
55
|
+
function resolveConfig(options, env2 = process.env, cwd = process.cwd()) {
|
|
23
56
|
const snap = () => snapshot(cwd);
|
|
24
|
-
const apiUrl = (options.apiUrl ??
|
|
25
|
-
const workspace = options.workspace ??
|
|
57
|
+
const apiUrl = (options.apiUrl ?? env2.PUBLIC_BCMS_API_URL ?? snap().apiUrl ?? "").replace(/\/+$/, "");
|
|
58
|
+
const workspace = options.workspace ?? env2.PUBLIC_BCMS_WORKSPACE ?? snap().workspace ?? "";
|
|
26
59
|
if (!apiUrl) throw required("apiUrl", "PUBLIC_BCMS_API_URL", `run the content fetch so ${SNAPSHOT_FILE} is present.`);
|
|
27
60
|
if (!workspace) {
|
|
28
61
|
throw required("workspace", "PUBLIC_BCMS_WORKSPACE", `run the content fetch so ${SNAPSHOT_FILE} is present.`);
|
|
@@ -30,9 +63,9 @@ function resolveConfig(options, env = process.env, cwd = process.cwd()) {
|
|
|
30
63
|
return {
|
|
31
64
|
apiUrl,
|
|
32
65
|
workspace,
|
|
33
|
-
projectId: options.projectId ??
|
|
34
|
-
mediaUrl: (options.mediaUrl ??
|
|
35
|
-
studioUrl: options.studioUrl ??
|
|
66
|
+
projectId: options.projectId ?? env2.PUBLIC_BCMS_PROJECT_ID ?? snap().projectId,
|
|
67
|
+
mediaUrl: (options.mediaUrl ?? env2.PUBLIC_BCMS_MEDIA_URL ?? DEFAULT_MEDIA_URL).replace(/\/+$/, ""),
|
|
68
|
+
studioUrl: options.studioUrl ?? env2.PUBLIC_BCMS_STUDIO_URL,
|
|
36
69
|
draftCookie: DRAFT_COOKIE,
|
|
37
70
|
draft: options.draft ?? true
|
|
38
71
|
};
|
|
@@ -75,8 +108,12 @@ function clientModule(cfg, buildCache) {
|
|
|
75
108
|
function virtualPlugin(cfg, buildCache) {
|
|
76
109
|
return {
|
|
77
110
|
name: "@bettercms-ai/astro:virtual",
|
|
111
|
+
// Before Vite's own resolver: `PUBLIC_CONFIG_ID` names a real file of this package, and a
|
|
112
|
+
// plugin that runs after the resolver is never asked about a specifier it already resolved.
|
|
113
|
+
enforce: "pre",
|
|
78
114
|
resolveId(id) {
|
|
79
115
|
if (id === CLIENT_ID || id === CONFIG_ID) return RESOLVED(id);
|
|
116
|
+
if (id === PUBLIC_CONFIG_ID) return RESOLVED(CONFIG_ID);
|
|
80
117
|
return null;
|
|
81
118
|
},
|
|
82
119
|
load(id) {
|
|
@@ -135,13 +172,13 @@ function bettercms(options = {}) {
|
|
|
135
172
|
* prerenders everything — on-demand rendering required `"server"` or `"hybrid"`, both
|
|
136
173
|
* of which fail this check. The adapter test stays as a second line of defence.
|
|
137
174
|
*/
|
|
138
|
-
"astro:config:done": ({ config, logger, ...rest }) => {
|
|
175
|
+
"astro:config:done": ({ config: config2, logger, ...rest }) => {
|
|
139
176
|
const buildOutput = rest.buildOutput;
|
|
140
|
-
const staticOutput = buildOutput !== void 0 ? buildOutput === "static" :
|
|
177
|
+
const staticOutput = buildOutput !== void 0 ? buildOutput === "static" : config2.output === "static" && !config2.adapter;
|
|
141
178
|
buildCache.on = buildCache.command === "build" && staticOutput;
|
|
142
179
|
decide = () => {
|
|
143
180
|
const late = rest.buildOutput;
|
|
144
|
-
const stillStatic = late !== void 0 ? late === "static" :
|
|
181
|
+
const stillStatic = late !== void 0 ? late === "static" : config2.output === "static" && !config2.adapter;
|
|
145
182
|
buildCache.on = buildCache.command === "build" && stillStatic;
|
|
146
183
|
buildCache.resolved = true;
|
|
147
184
|
if (buildCache.on) {
|
|
@@ -171,7 +208,7 @@ var integration_default = bettercms;
|
|
|
171
208
|
|
|
172
209
|
// src/runtime.ts
|
|
173
210
|
import { createClient } from "@bettercms-ai/sdk";
|
|
174
|
-
import { readPublishedContent, publishedEntry } from "@bettercms-ai/sdk";
|
|
211
|
+
import { readPublishedContent as readPublishedContent2, publishedEntry } from "@bettercms-ai/sdk";
|
|
175
212
|
function liveSrc(opts) {
|
|
176
213
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
177
214
|
const q = new URLSearchParams({ key: opts.apiKey });
|