@bettercms-ai/astro 0.13.0 → 0.14.1
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 +60 -5
- package/components/BcmsForm.astro +80 -0
- package/components/BcmsVisualEditing.astro +4 -5
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -29,12 +29,28 @@ export default defineConfig({
|
|
|
29
29
|
});
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
### Environment variables
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
Every public value resolves in this order: `bettercms({ ... })` option, then env, then the
|
|
35
|
+
`bcms-content.json` snapshot the deploy workflow writes before the build.
|
|
36
|
+
|
|
37
|
+
| Variable | Option | Read | Default / notes |
|
|
38
|
+
| --- | --- | --- | --- |
|
|
39
|
+
| `PUBLIC_BCMS_API_URL` | `apiUrl` | config time | Required (option, env or snapshot) |
|
|
40
|
+
| `PUBLIC_BCMS_WORKSPACE` | `workspace` | config time | Required (option, env or snapshot) |
|
|
41
|
+
| `PUBLIC_BCMS_PROJECT_ID` | `projectId` | config time | Optional project scope for forms |
|
|
42
|
+
| `PUBLIC_BCMS_MEDIA_URL` | `mediaUrl` | config time | `https://cdn.bettercms.ai` |
|
|
43
|
+
| `PUBLIC_BCMS_STUDIO_URL` | `studioUrl` | config time | Unset. `<BcmsVisualEditing>` falls back to `https://bettercms.ai` |
|
|
44
|
+
| `BCMS_API_KEY` | none | server runtime | `content:read`, or `content:read:draft` for draft previews. Never bundled |
|
|
45
|
+
| `BCMS_DRAFT_SECRET` | none | server runtime | Signs the draft cookie. Runtime `process.env` wins over a build-time value. Without it `/api/bcms/draft/enable` returns 500 |
|
|
46
|
+
|
|
47
|
+
The BetterCMS-generated deploy workflow sets `PUBLIC_BCMS_API_URL`, `PUBLIC_BCMS_WORKSPACE`
|
|
48
|
+
and `PUBLIC_BCMS_PROJECT_ID` for the build. Its repo secret `BCMS_API_KEY` reaches only the
|
|
49
|
+
content-fetch step, never the build. On your own host, set the two server variables yourself.
|
|
50
|
+
|
|
51
|
+
This package ships no revalidation route. Point the project's revalidation webhook
|
|
52
|
+
(Project → Settings) at your rebuild hook. Its secret (the dashboard calls it
|
|
53
|
+
`BETTERCMS_WEBHOOK_SECRET`) signs the body as `x-bettercms-signature: sha256=<hmac>`.
|
|
38
54
|
|
|
39
55
|
Add the virtual-module + `Astro.locals` types to `src/env.d.ts`:
|
|
40
56
|
|
|
@@ -116,6 +132,25 @@ or on-demand-revalidation hook.
|
|
|
116
132
|
|
|
117
133
|
Markup is class-driven and unstyled — you own the CSS.
|
|
118
134
|
|
|
135
|
+
### Phone fields with a country code picker
|
|
136
|
+
|
|
137
|
+
A phone field whose author enabled **Country code picker** in the builder renders a country
|
|
138
|
+
control beside the number. Whatever the visitor picks, the field still submits **one value**,
|
|
139
|
+
under its own key, in E.164 (`+14155550123`) — there is no second key to handle.
|
|
140
|
+
|
|
141
|
+
Two classes are yours to style:
|
|
142
|
+
|
|
143
|
+
| Class | Element |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `bcms-phone` | the wrapper around the country control and the number |
|
|
146
|
+
| `bcms-phone-country` | the country control itself |
|
|
147
|
+
|
|
148
|
+
The country control is a native `<select>` built at build/SSR time, so **no phone library
|
|
149
|
+
reaches the visitor's browser**. The trade is that the number is not reformatted as it is
|
|
150
|
+
typed — the value is still composed correctly on submit. `@bettercms-ai/next` makes the other
|
|
151
|
+
choice and ships a formatter in a lazy chunk.
|
|
152
|
+
|
|
153
|
+
|
|
119
154
|
## Live editing on a static site — `<BcmsLiveBlocks>`
|
|
120
155
|
|
|
121
156
|
When the Visual Editor frames a static build it can only show what was built, so editing a
|
|
@@ -170,3 +205,23 @@ The integration injects `/api/bcms/draft/enable?token=<jwt>&redirect=/path` and
|
|
|
170
205
|
`/api/bcms/draft/disable`. Generate the preview-token link from the dashboard;
|
|
171
206
|
visiting `enable` validates the token against the backend, sets a signed cookie,
|
|
172
207
|
and subsequent loads return draft content. Disable with the `disable` route.
|
|
208
|
+
|
|
209
|
+
### Two draft lanes
|
|
210
|
+
|
|
211
|
+
| Lane | Build | How the canvas shows a draft |
|
|
212
|
+
| --- | --- | --- |
|
|
213
|
+
| SSR | `output: "server"` + the draft routes above + `BCMS_DRAFT_SECRET` + a `content:read:draft` `BCMS_API_KEY` | The loaders read the draft on every request, so hand-written layouts preview too |
|
|
214
|
+
| Static | Any static build, with the page body rendered through `<BcmsLiveBlocks>` | The editor pushes the draft block tree to the island over `postMessage`. No fetch, no key in the page. Only the `BcmsLiveBlocks` region updates |
|
|
215
|
+
|
|
216
|
+
## Headless projects (your own host)
|
|
217
|
+
|
|
218
|
+
BetterCMS never builds, releases or deploys a headless project. Any build, artifact or deploy
|
|
219
|
+
request for one returns **409 `HEADLESS_NO_BUILD`**.
|
|
220
|
+
|
|
221
|
+
- **Deploy:** push to your repository. Your provider (Vercel, Netlify or Cloudflare Pages)
|
|
222
|
+
builds it. For fresh content on publish, point the revalidation webhook at the provider's
|
|
223
|
+
rebuild hook.
|
|
224
|
+
- **Draft:** use your framework's draft route on the provider's **preview** deployment URL.
|
|
225
|
+
For Astro that is the SSR lane above: `output: "server"`, the injected
|
|
226
|
+
`/api/bcms/draft/*` routes, `BCMS_DRAFT_SECRET`, and a `BCMS_API_KEY` carrying
|
|
227
|
+
`content:read:draft`. Keep the draft key off production.
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import config from "bettercms:config";
|
|
11
11
|
import type { DeliveryForm, DeliveryFormField } from "@bettercms-ai/sdk";
|
|
12
|
+
import { getCountries, getCountryCallingCode, isSupportedCountry, getExampleNumber } from "libphonenumber-js/min";
|
|
13
|
+
import phoneExamples from "libphonenumber-js/mobile/examples";
|
|
12
14
|
|
|
13
15
|
interface Props {
|
|
14
16
|
form: DeliveryForm;
|
|
@@ -32,6 +34,46 @@ const INPUT_TYPE: Partial<Record<DeliveryFormField["type"], string>> = {
|
|
|
32
34
|
url: "url",
|
|
33
35
|
};
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Calling-code options for a phone picker. Frontmatter only — this runs at build/SSR time and
|
|
39
|
+
* ships ZERO bytes of libphonenumber to the visitor; the inline script below never sees it.
|
|
40
|
+
*
|
|
41
|
+
* Every libphonenumber helper is wrapped in an arrow rather than passed as a bare callback:
|
|
42
|
+
* `.filter`/`.map` hand it the array INDEX as its `metadata` argument and it throws.
|
|
43
|
+
*/
|
|
44
|
+
const REGION_NAMES =
|
|
45
|
+
typeof Intl !== "undefined" && typeof Intl.DisplayNames === "function"
|
|
46
|
+
? new Intl.DisplayNames(["en"], { type: "region" })
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
let countryCache: { iso: string; name: string; code: string; trunk: boolean }[] | null = null;
|
|
50
|
+
const countryList = () => {
|
|
51
|
+
if (countryCache) return countryCache;
|
|
52
|
+
countryCache = getCountries()
|
|
53
|
+
.filter((iso) => isSupportedCountry(iso))
|
|
54
|
+
.map((iso) => {
|
|
55
|
+
let name = iso as string;
|
|
56
|
+
try {
|
|
57
|
+
name = REGION_NAMES?.of(iso) ?? iso;
|
|
58
|
+
} catch {
|
|
59
|
+
name = iso;
|
|
60
|
+
}
|
|
61
|
+
let trunk = false;
|
|
62
|
+
try {
|
|
63
|
+
const ex = getExampleNumber(iso, phoneExamples);
|
|
64
|
+
if (ex) {
|
|
65
|
+
const national = ex.formatNational().replace(/\D/g, "");
|
|
66
|
+
trunk = national.length > String(ex.nationalNumber).length && national.startsWith("0");
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
trunk = false;
|
|
70
|
+
}
|
|
71
|
+
return { iso: iso as string, name, code: getCountryCallingCode(iso), trunk };
|
|
72
|
+
})
|
|
73
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
74
|
+
return countryCache;
|
|
75
|
+
};
|
|
76
|
+
|
|
35
77
|
const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
36
78
|
---
|
|
37
79
|
|
|
@@ -98,6 +140,29 @@ const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
|
98
140
|
<select id={`bcms-f-${f.key}`} name={f.key} required={f.required}>
|
|
99
141
|
{(f.options ?? []).map((o) => <option value={o} data-bcms-form-part="option">{o}</option>)}
|
|
100
142
|
</select>
|
|
143
|
+
) : f.type === "phone" && f.ui?.countryPicker ? (
|
|
144
|
+
/* A country select plus a tel input, submitting ONE E.164 value. `required` belongs
|
|
145
|
+
on the number only — a select always has a value and would never block. */
|
|
146
|
+
<span class="bcms-phone">
|
|
147
|
+
<select class="bcms-phone-country" name={`${f.key}__country`} aria-label="Country code">
|
|
148
|
+
<option value="" selected={!f.ui.defaultCountry}>International</option>
|
|
149
|
+
{countryList().map((c) => (
|
|
150
|
+
<option value={c.iso} data-cc={c.code} data-trunk={c.trunk ? "1" : undefined} selected={c.iso === f.ui!.defaultCountry}>
|
|
151
|
+
{c.name} +{c.code}
|
|
152
|
+
</option>
|
|
153
|
+
))}
|
|
154
|
+
</select>
|
|
155
|
+
<input
|
|
156
|
+
type="tel"
|
|
157
|
+
id={`bcms-f-${f.key}`}
|
|
158
|
+
name={f.key}
|
|
159
|
+
placeholder={f.placeholder}
|
|
160
|
+
value={f.defaultValue}
|
|
161
|
+
required={f.required}
|
|
162
|
+
autocomplete="tel"
|
|
163
|
+
dir="ltr"
|
|
164
|
+
/>
|
|
165
|
+
</span>
|
|
101
166
|
) : (
|
|
102
167
|
<input
|
|
103
168
|
type={INPUT_TYPE[f.type] ?? "text"}
|
|
@@ -126,6 +191,13 @@ const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
|
126
191
|
{showTurnstile && <script is:inline src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>}
|
|
127
192
|
|
|
128
193
|
<script is:inline>
|
|
194
|
+
// Compose one E.164 value from a country-picker phone field. CANONICAL COPY is
|
|
195
|
+
// PHONE_COMPOSE_JS in the backend's src/lib/sites/render-page.ts, and this file is pinned
|
|
196
|
+
// against it byte for byte by src/__tests__/content/form-script-pin.test.ts. All three
|
|
197
|
+
// vanilla lanes are inline script text on someone else's page and cannot import at runtime,
|
|
198
|
+
// so the rule genuinely exists more than once.
|
|
199
|
+
function bcmsPhone(cc,raw,trunk){var d=String(raw||'').trim();if(!d)return '';if(d.charAt(0)==='+'){var i=d.replace(/\D/g,'');return i?'+'+i:'';}if(!cc)return d;var n=d.replace(/\D/g,'');if(trunk&&n.charAt(0)==='0')n=n.slice(1);return n?'+'+cc+n:'';}
|
|
200
|
+
|
|
129
201
|
// One copy of this script ships per <BcmsForm> instance, and each copy sees EVERY
|
|
130
202
|
// form on the page — so two forms meant four bindings and a double submit. The
|
|
131
203
|
// `bcmsFormBound` flag makes binding idempotent no matter how many copies run,
|
|
@@ -177,6 +249,14 @@ const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
|
177
249
|
if (k in data) data[k] = Array.isArray(data[k]) ? [...data[k], v] : [data[k], v];
|
|
178
250
|
else data[k] = v;
|
|
179
251
|
});
|
|
252
|
+
// Compose AFTER the FormData loop. A showIf pointing at a phone key must keep comparing
|
|
253
|
+
// the raw digits, and applyConds runs on every input event.
|
|
254
|
+
form.querySelectorAll('select[name$="__country"]').forEach((sel) => {
|
|
255
|
+
const k = sel.name.slice(0, -9);
|
|
256
|
+
const o = sel.options[sel.selectedIndex];
|
|
257
|
+
data[k] = bcmsPhone(o ? (o.getAttribute("data-cc") || "") : "", data[k], o ? o.getAttribute("data-trunk") === "1" : false);
|
|
258
|
+
delete data[k + "__country"];
|
|
259
|
+
});
|
|
180
260
|
const tsEl = form.querySelector('[name="cf-turnstile-response"]');
|
|
181
261
|
const body = { data };
|
|
182
262
|
if (tsEl?.value) body["cf-turnstile-response"] = tsEl.value;
|
|
@@ -16,11 +16,10 @@
|
|
|
16
16
|
interface Props {
|
|
17
17
|
studioUrl?: string;
|
|
18
18
|
}
|
|
19
|
-
// `
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
const { studioUrl = "https://demo.bettercms.ai" } = Astro.props;
|
|
19
|
+
// `bettercms.ai` is the studio origin — the one the dashboard's integration snippets hand out
|
|
20
|
+
// as `PUBLIC_BCMS_STUDIO_URL`, and the default of @bettercms-ai/next's <VisualEditing>.
|
|
21
|
+
// `demo.bettercms.ai` is a stale alias; `dashboard.bettercms.ai` resolves but 404s.
|
|
22
|
+
const { studioUrl = "https://bettercms.ai" } = Astro.props;
|
|
24
23
|
const src = `${studioUrl.replace(/\/+$/, "")}/bcms-visual-editing.js`;
|
|
25
24
|
---
|
|
26
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettercms-ai/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
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",
|
|
@@ -54,11 +54,12 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@bettercms-ai/image-url": "^0.3.0",
|
|
57
|
-
"@bettercms-ai/
|
|
57
|
+
"@bettercms-ai/next": "^0.13.0",
|
|
58
|
+
"@bettercms-ai/richtext": "^0.3.0",
|
|
59
|
+
"@bettercms-ai/sdk": "^1.21.0",
|
|
58
60
|
"@bettercms-ai/types": "^1.11.0",
|
|
59
61
|
"@bettercms-ai/ui": "^0.7.0",
|
|
60
|
-
"
|
|
61
|
-
"@bettercms-ai/next": "^0.12.1"
|
|
62
|
+
"libphonenumber-js": "^1.13.13"
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
64
65
|
"astro": ">=4",
|