@bettercms-ai/next 0.27.1 → 0.29.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/dist/bettercms-snapshot.js +35 -5
- package/dist/bettercms-snapshot.js.map +1 -1
- package/dist/blocks.js +1 -1
- package/dist/blocks.js.map +1 -1
- package/dist/draft-bridge.js +1 -1
- package/dist/draft-bridge.js.map +1 -1
- package/dist/elements.d.ts +51 -0
- package/dist/elements.js +130 -0
- package/dist/elements.js.map +1 -0
- package/dist/form.d.ts +1 -1
- package/dist/form.js +1 -1
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +69 -46
- package/dist/index.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/search.js +1 -1
- package/dist/search.js.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/field.tsx","../src/image.tsx","../src/button.tsx"],"sourcesContent":["/**\n * <BcmsField> — tag an entry field's rendered value with its identity so the\n * BetterCMS visual editor can bind click-to-edit deterministically (mirrors\n * Sanity's `data-sanity` / BaseHub's `data-basehub`). Entry-backed templates\n * hand-render field values (`f.title`, `f.body.html`), so the field `path` is\n * declared here by the author — the delivery payload carries no source-map.\n *\n * Server-safe (no hooks): renders in a Next Server Component. When the child is ONE\n * DOM element (`<h1>`, `<p>`), the attributes go on that element and nothing is added to\n * the page: `.hero > h1` and every animation target stay what the author wrote. Text,\n * several children, or a component (which may not pass unknown props on) are wrapped in a\n * `display:contents` span as before; pass `as` to carry the attributes on a real wrapper.\n * Twin: BcmsField.astro in @bettercms-ai/astro.\n *\n * `path` follows the editor grammar: `title` · `hero.title` · `columns[0].links[2].href`\n * (dotted keys and any number of `[n]` indices). For images, put the attributes on the\n * `<img>` itself (`kind=\"image\"` reflection patches `.src`), not on a wrapper:\n * <img src={imageValueUrl(f.cover, { w: 1200 })} data-bcms-field=\"cover\" data-bcms-kind=\"image\" />\n * `imageValueUrl` (re-exported from this package) applies the value's per-placement `crop` and\n * `focal`; `.url` alone would drop them. For a cover-fit box add\n * `style={{ objectPosition: imageValueObjectPosition(f.cover) ?? undefined }}`.\n */\nimport { Children, cloneElement, isValidElement, type ElementType, type ReactElement, type ReactNode } from \"react\";\n\nexport interface BcmsFieldProps {\n /** Field path in the editor grammar, e.g. `title` or `facts[2].label`. */\n path: string;\n /** Kind hint the editor's reflection branches on. Omit for plain text/number. */\n kind?: \"richtext\" | \"image\";\n /** Wrapper element. Without it: no wrapper around a single DOM element, a `display:contents` span around anything else. */\n as?: ElementType;\n children: ReactNode;\n}\n\nexport function BcmsField({ path, kind, as, children }: BcmsFieldProps) {\n const Tag = as ?? \"span\";\n const only = as ? [] : Children.toArray(children);\n const sole = only.length === 1 && isValidElement(only[0]) && typeof only[0].type === \"string\" ? (only[0] as ReactElement<Record<string, unknown>>) : null;\n // An element that already declares a field keeps its own address: wrapped, as before.\n if (sole && sole.props[\"data-bcms-field\"] === undefined && sole.props[\"data-bcms-layout-field\"] === undefined) {\n return cloneElement(sole, { \"data-bcms-field\": path, \"data-bcms-kind\": kind });\n }\n return (\n <Tag\n data-bcms-field={path}\n data-bcms-kind={kind}\n style={as ? undefined : { display: \"contents\" }}\n >\n {children}\n </Tag>\n );\n}\n","/**\n * <BcmsImage> — render an image field as one responsive `<img>` on the BetterCMS media transform\n * endpoint, declared so the visual editor binds it and a publish writes the new picture into it.\n * The same markup as the Astro SDK's <BcmsImage>.\n *\n * <BcmsImage path=\"hero.image\" src={page.hero.image} sizes=\"(min-width: 800px) 50vw, 100vw\" />\n *\n * `src` is an image VALUE (`{ url, altText, width?, focal?, crop? }`), a media url or an asset id.\n * The value's per-placement crop and focal point ride every url, and the focal point is also the\n * `object-position` (inert without `object-fit`). With `sizes` and no `width` the srcset is a WIDTH\n * ladder, never wider than the picture; with a pixel `width` it is 1x/2x. The value's `altText` is\n * the alt unless `alt` says otherwise. Any other prop reaches the `<img>` as written.\n *\n * A plain `<img>`, deliberately: `next/image` refuses a remote host the app has not allow-listed,\n * so an editor's replacement would break the build. Server-safe (no hooks).\n */\nimport imageUrlBuilder, {\n focalInCrop,\n normalizeImageCrop,\n normalizeImageFocal,\n objectPosition,\n type ImageFit,\n type ImageFormat,\n type ImageSource,\n} from \"@bettercms-ai/image-url\";\nimport type { CSSProperties, ImgHTMLAttributes } from \"react\";\n\n/** What delivery adds to an image value beside its url: the facts this component reads. */\ntype ImageFacts = { altText?: string | null; alt?: string | null; width?: number | null; height?: number | null; focalX?: number | null; focalY?: number | null };\n\nexport interface BcmsImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"srcSet\" | \"width\" | \"height\"> {\n src: ImageSource | (Exclude<ImageSource, string> & ImageFacts);\n /** The field this image renders, in the editor grammar (`cover`, `hero.image`). */\n path?: string;\n width?: number;\n height?: number;\n format?: ImageFormat;\n quality?: number;\n fit?: ImageFit;\n /** Focal point override, 0–1. Normally omitted: the value and the asset carry their own. */\n focalX?: number | null;\n focalY?: number | null;\n /** Media host for a `src` that is only an asset id. Defaults to the platform's. */\n baseUrl?: string;\n}\n\n/** The widths a `sizes` image offers: phone to desktop, and never an upscale of the picture. */\nconst LADDER = [480, 768, 1080, 1440, 1920];\n\nconst num = (v: unknown): number | undefined => (typeof v === \"number\" && Number.isFinite(v) ? v : undefined);\n\nexport function BcmsImage({\n src,\n path,\n alt,\n width,\n height,\n format = \"webp\",\n quality,\n fit,\n sizes,\n loading = \"lazy\",\n focalX,\n focalY,\n baseUrl,\n style,\n ...rest\n}: BcmsImageProps) {\n // An explicit prop wins; then the value's own `focal`; then the asset's focalX/focalY. The last\n // two are SOURCE-relative, so with a crop they are re-expressed inside it.\n const asset = (typeof src === \"object\" && src !== null ? src : {}) as Record<string, unknown>;\n const explicit = focalX != null || focalY != null;\n const stored =\n normalizeImageFocal(asset.focal) ??\n (num(asset.focalX) != null || num(asset.focalY) != null ? { x: num(asset.focalX) ?? 0.5, y: num(asset.focalY) ?? 0.5 } : null);\n const inWindow = stored ? focalInCrop(stored, normalizeImageCrop(asset.crop)) : null;\n const fx = explicit ? focalX : inWindow?.x;\n const fy = explicit ? focalY : inWindow?.y;\n const hasFocal = fx != null || fy != null;\n\n const builder = imageUrlBuilder({ baseUrl });\n const make = () => {\n let b = builder(src);\n if (width) b = b.width(width);\n if (height) b = b.height(height);\n if (format) b = b.format(format);\n if (quality) b = b.quality(quality);\n if (fit) b = b.fit(fit);\n if (hasFocal) b = b.focal(fx ?? 0.5, fy ?? 0.5);\n return b;\n };\n const src1x = make().url();\n const own = num(asset.width);\n const ladder = own ? [...new Set([...LADDER.filter((w) => w < own), Math.min(own, LADDER[LADDER.length - 1]!)])] : LADDER;\n const srcSet =\n sizes && !width ? ladder.map((w) => `${make().width(w).url()} ${w}w`).join(\", \") : `${src1x} 1x, ${make().dpr(2).url()} 2x`;\n // A pixel width is the author's size for THIS image, so it also goes on as a style (a site rule\n // like `img { width: 100% }` beats a bare attribute), capped at the column it sits in.\n const merged: CSSProperties = {\n ...(width ? { width: `${width}px`, maxWidth: \"100%\", height: \"auto\" } : {}),\n ...(hasFocal ? { objectPosition: objectPosition({ x: fx, y: fy }) } : {}),\n ...style,\n };\n\n return (\n <img\n src={src1x}\n srcSet={srcSet}\n alt={alt ?? (typeof asset.altText === \"string\" ? asset.altText : typeof asset.alt === \"string\" ? asset.alt : \"\")}\n data-bcms-field={path}\n data-bcms-kind={path ? \"image\" : undefined}\n width={width}\n height={height}\n sizes={sizes}\n loading={loading}\n style={Object.keys(merged).length > 0 ? merged : undefined}\n {...rest}\n />\n );\n}\n","/**\n * <BcmsButton> — render a `button` field ({ label, href, variant?, size?, arrow?, icon?,\n * newTab?, fullWidth? }) as one editable button, with the hooks the BetterCMS visual editor repaints and\n * in-place publishing writes. The same markup as the Astro SDK's <BcmsButton>:\n *\n * <a data-bcms-button=\"<path>\" data-variant data-size [data-full-width] href [target rel] data-bcms-props=\"<path>.href|url|href\">\n * [<svg class=\"bcms-btn-icon\">]<span data-bcms-field=\"<path>.label\">Label</span>[<svg class=\"bcms-btn-arrow\">]\n * </a>\n *\n * A value with `action: \"code\"` (no href: the site's script handles the click) renders the same\n * children in `<button type=\"button\" data-bcms-button=\"<path>\" data-variant data-size>`.\n *\n * Style it by `data-variant` and `data-size` in your own CSS. A full-width button also carries\n * `display:flex;width:100%;justify-content:center` inline, so it stretches with no rule of yours.\n * Server-safe (no hooks).\n *\n * <BcmsButton path=\"hero.cta\" value={page.hero.cta} className=\"btn\" />\n */\nimport { formatPropsAttribute, readButtonValue } from \"@bettercms-ai/types\";\nimport { Icon } from \"@bettercms-ai/icons/map\";\n\n/** A full-width button stretches across its column, label centred: the hosted renderer's `bcms-btn-full`. */\nconst FULL_WIDTH_STYLE = { display: \"flex\", width: \"100%\", justifyContent: \"center\" } as const;\n\nexport interface BcmsButtonProps {\n /** The field path in the editor grammar, e.g. `cta` or `hero.cta`. */\n path: string;\n /** The stored value. Missing keys render as the defaults (Primary, medium, no arrow, no icon). */\n value: unknown;\n className?: string;\n}\n\nexport function BcmsButton({ path, value, className }: BcmsButtonProps) {\n const b = readButtonValue(value);\n if (!b.label) return null;\n const content = (\n <>\n {/* Direct children of the root, as the editor and in-place publishing find them. */}\n {b.icon && <Icon symbol={b.icon} className=\"bcms-btn-icon\" aria-hidden focusable=\"false\" />}\n <span data-bcms-field={`${path}.label`} data-bcms-kind=\"text\">\n {b.label}\n </span>\n {b.arrow && <Icon symbol=\"arrow-right\" className=\"bcms-btn-arrow\" aria-hidden focusable=\"false\" />}\n </>\n );\n // `action: \"code\"`: your own handler owns the click (delegate on `[data-bcms-button]`), so no href.\n if (b.action === \"code\") {\n return (\n <button type=\"button\" className={className} data-bcms-button={path} data-variant={b.variant} data-size={b.size} {...(b.fullWidth ? { \"data-full-width\": \"\", style: FULL_WIDTH_STYLE } : {})}>\n {content}\n </button>\n );\n }\n return (\n <a\n className={className}\n href={b.href || \"#\"}\n data-bcms-button={path}\n data-variant={b.variant}\n data-size={b.size}\n {...(b.fullWidth ? { \"data-full-width\": \"\", style: FULL_WIDTH_STYLE } : {})}\n data-bcms-props={formatPropsAttribute([{ address: `${path}.href`, kind: \"url\", attr: \"href\" }])}\n {...(b.newTab ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {})}\n >\n {content}\n </a>\n );\n}\n"],"mappings":"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","names":["jsx","jsx"]}
|
package/dist/form.d.ts
CHANGED
package/dist/form.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
formInitialValues,
|
|
10
10
|
asStringArray,
|
|
11
11
|
toggleOption
|
|
12
|
-
} from "@bettercms-ai/sdk";
|
|
12
|
+
} from "@bettercms-ai/sdk/read";
|
|
13
13
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
var TURNSTILE_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
15
15
|
var PhoneField = lazy(() => import("./phone-field.js"));
|
package/dist/form.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/form.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * <BcmsForm> — render a BetterCMS form natively in React, the headless way.\n *\n * Pass a form straight from your content snapshot (`readSnapshot().forms[n]`) or\n * the delivery API. It renders real inputs (no iframe), handles conditional\n * `showIf` fields, URL-query prefill, the honeypot, an optional Turnstile widget,\n * and submits via `@bettercms-ai/sdk`'s `submitForm`. All visible markup is\n * unstyled and class-driven, so the host site owns the look.\n */\n\nimport { lazy, Suspense, useEffect, useMemo, useRef, useState, type FormEvent, type ReactElement } from \"react\";\nimport {\n submitForm,\n shouldShowField,\n formInitialValues,\n asStringArray,\n toggleOption,\n type DeliveryForm,\n type DeliveryFormField,\n type FormValue,\n type FormValues,\n type FormSubmitError,\n} from \"@bettercms-ai/sdk\";\n\nconst TURNSTILE_SRC = \"https://challenges.cloudflare.com/turnstile/v0/api.js\";\n\n/**\n * The country-picker phone control, loaded only when a form actually has one.\n *\n * It carries libphonenumber's metadata (~80KB), which no form without a picker should pay\n * for. The import specifier must survive into `dist/form.js` rather than being inlined —\n * see the `external` entry in tsup.config.ts and the dist greps in the tests.\n */\nconst PhoneField = lazy(() => import(\"./phone-field.js\"));\n\n// Field type → native <input type>. Other types render their own element.\nconst INPUT_TYPES: Partial<Record<DeliveryFormField[\"type\"], string>> = {\n text: \"text\",\n email: \"email\",\n phone: \"tel\",\n url: \"url\",\n number: \"number\",\n date: \"date\",\n};\n\nexport interface BcmsFormProps {\n /** A form from your content snapshot or the delivery API. */\n form: DeliveryForm;\n /** API origin. Defaults to the SDK default (https://api.bettercms.ai). */\n apiBase?: string;\n /** Turnstile site key — needed to render the widget when `form.turnstileEnabled`. */\n turnstileSiteKey?: string;\n /** Called with the new submission id after a successful submit (before any redirect). */\n onSubmitted?: (submissionId: string) => void;\n /** Class for the <form> element; field wrappers/status use `bcms-*` classes. */\n className?: string;\n}\n\ntype Status = { state: \"idle\" | \"submitting\" | \"error\" | \"success\"; message?: string };\n\nfunction readPrefill(): Record<string, string> {\n if (typeof window === \"undefined\") return {};\n const out: Record<string, string> = {};\n new URLSearchParams(window.location.search).forEach((v, k) => {\n out[k] = v;\n });\n return out;\n}\n\ntype Page = { label: string | null; key: string | null; fields: DeliveryFormField[] };\n\n/**\n * Cut the flat field list into pages at its `step` dividers, or null when it has none. Fields\n * before the first divider are page 1; an empty page is dropped. The hosted renderer and\n * BcmsForm.astro carry the same rule: each package publishes on its own, so none of them can\n * count on a newer shared helper arriving with it.\n */\nfunction splitSteps(fields: DeliveryFormField[]): Page[] | null {\n if (!fields.some((f) => f.type === \"step\")) return null;\n const pages: Page[] = [];\n let cur: Page = { label: null, key: null, fields: [] };\n for (const f of fields) {\n if (f.type !== \"step\") {\n cur.fields.push(f);\n continue;\n }\n if (cur.fields.length) pages.push(cur);\n cur = { label: f.label, key: f.key, fields: [] };\n }\n if (cur.fields.length) pages.push(cur);\n return pages;\n}\n\nfunction resetTurnstile(): void {\n const t = (globalThis as { turnstile?: { reset: () => void } }).turnstile;\n if (t) {\n try {\n t.reset();\n } catch {\n /* widget not mounted yet */\n }\n }\n}\n\nexport function BcmsForm({\n form,\n apiBase,\n turnstileSiteKey,\n onSubmitted,\n className,\n}: BcmsFormProps): ReactElement {\n const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);\n const [values, setValues] = useState<FormValues>(() => {\n const init = formInitialValues(form, readPrefill());\n if (form.honeypotField) init[form.honeypotField] = \"\";\n return init;\n });\n const [status, setStatus] = useState<Status>({ state: \"idle\" });\n const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});\n\n // Steps. Until the component has mounted every page is shown, so server-rendered output (and a\n // visitor without JavaScript) gets one long form; `enhanced` then turns it into pages.\n const pages = useMemo(() => splitSteps(form.fields), [form.fields]);\n const [enhanced, setEnhanced] = useState(false);\n const [step, setStep] = useState(0);\n const pageRefs = useRef<(HTMLFieldSetElement | null)[]>([]);\n const headingRefs = useRef<(HTMLHeadingElement | null)[]>([]);\n const moveFocus = useRef(false);\n const last = pages ? pages.length - 1 : 0;\n const paged = Boolean(pages && enhanced);\n useEffect(() => setEnhanced(true), []);\n // Focus follows the visitor to the new page's heading, never on first paint.\n useEffect(() => {\n if (!moveFocus.current) return;\n moveFocus.current = false;\n headingRefs.current[step]?.focus();\n }, [step]);\n\n // The browser's own validity, for ONE page: the first control that fails reports its message.\n function pageInvalid(i: number): boolean {\n const controls = pageRefs.current[i]?.querySelectorAll<HTMLInputElement>(\"input,select,textarea\") ?? [];\n for (const c of controls) {\n if (!c.checkValidity()) {\n c.reportValidity();\n return true;\n }\n }\n return false;\n }\n function goTo(i: number): void {\n moveFocus.current = true;\n setStep(i);\n }\n function next(): void {\n if (!pageInvalid(step)) goTo(step + 1);\n }\n\n // Cloudflare auto-renders any `.cf-turnstile` element once its script is present.\n useEffect(() => {\n if (!showTurnstile || document.querySelector(`script[src=\"${TURNSTILE_SRC}\"]`)) return;\n const script = document.createElement(\"script\");\n script.src = TURNSTILE_SRC;\n script.async = true;\n script.defer = true;\n document.head.appendChild(script);\n }, [showTurnstile]);\n\n const visibleFields = useMemo(\n () => form.fields.filter((f) => f.type !== \"hidden\" && f.type !== \"step\" && shouldShowField(f, values)),\n [form.fields, values],\n );\n\n const setValue = (key: string, value: FormValue) =>\n setValues((prev) => ({ ...prev, [key]: value }));\n\n async function handleSubmit(e: FormEvent<HTMLFormElement>): Promise<void> {\n e.preventDefault();\n // Enter inside a field before the last page moves on rather than submitting half a form.\n if (paged && step < last) return next();\n setStatus({ state: \"submitting\" });\n setFieldErrors({});\n\n const tokenEl = e.currentTarget.querySelector<HTMLInputElement>(\n '[name=\"cf-turnstile-response\"]',\n );\n const turnstileToken = tokenEl?.value || undefined;\n\n // Drop conditionally-hidden fields so a hidden required field can't block submit;\n // keep hidden-type fields and the honeypot (the API classifies bots on it).\n const data: FormValues = {};\n for (const field of form.fields) {\n if (field.type === \"step\") continue; // a divider is never an answer\n if (field.type === \"hidden\" || shouldShowField(field, values)) {\n data[field.key] = values[field.key];\n }\n }\n if (form.honeypotField) data[form.honeypotField] = values[form.honeypotField] ?? \"\";\n\n try {\n const { id } = await submitForm({ formId: form.id, data, turnstileToken, baseUrl: apiBase });\n onSubmitted?.(id);\n if (form.redirectUrl) {\n window.location.href = form.redirectUrl;\n return;\n }\n const reset = formInitialValues(form);\n if (form.honeypotField) reset[form.honeypotField] = \"\";\n setValues(reset);\n setStep(0);\n setStatus({\n state: \"success\",\n message: form.successMessage ?? \"Thanks! Your submission was received.\",\n });\n resetTurnstile();\n } catch (err) {\n const e2 = err as FormSubmitError;\n if (e2.fieldErrors) {\n setFieldErrors(e2.fieldErrors);\n // The server found a problem on a page the visitor has left: take them back to it.\n const bad = pages?.findIndex((p) => p.fields.some((f) => e2.fieldErrors?.[f.key]));\n if (bad !== undefined && bad >= 0) goTo(bad);\n }\n setStatus({ state: \"error\", message: e2.message || \"Something went wrong. Please try again.\" });\n resetTurnstile();\n }\n }\n\n return (\n // `data-bcms-form` carries the id so the Visual Editor can find THIS form in the\n // preview frame and patch its copy live. Valued rather than bare: the runtime\n // selectors are `form[data-bcms-form]`, which a valued attribute still matches.\n <form className={className} onSubmit={handleSubmit} noValidate data-bcms-form={form.id}>\n {pages\n ? pages.map((page, i) => (\n <fieldset\n key={page.key ?? \"first\"}\n ref={(el) => {\n pageRefs.current[i] = el;\n }}\n className=\"bcms-step\"\n data-bcms-step={i + 1}\n hidden={paged && i !== step}\n aria-labelledby={`bcms-step-${form.id}-${i + 1}`}\n >\n <h3\n id={`bcms-step-${form.id}-${i + 1}`}\n className=\"bcms-step-title\"\n tabIndex={-1}\n ref={(el) => {\n headingRefs.current[i] = el;\n }}\n data-bcms-form-field={page.key ?? undefined}\n >\n <span className=\"bcms-step-count\">\n Step {i + 1} of {pages.length}\n </span>\n {page.label ? (\n <>\n {\" \"}\n <span data-bcms-form-part=\"label\">{page.label}</span>\n </>\n ) : null}\n </h3>\n {visibleFields\n .filter((f) => page.fields.includes(f))\n .map((field) => (\n <Field\n key={field.key}\n field={field}\n value={values[field.key]}\n error={fieldErrors[field.key]}\n onChange={(v) => setValue(field.key, v)}\n />\n ))}\n {paged ? (\n <div className=\"bcms-step-nav\">\n {i > 0 ? (\n <button type=\"button\" data-bcms-step-back onClick={() => goTo(i - 1)}>\n Back\n </button>\n ) : null}\n {i < last ? (\n <button type=\"button\" data-bcms-step-next onClick={next}>\n Next\n </button>\n ) : null}\n </div>\n ) : null}\n </fieldset>\n ))\n : visibleFields.map((field) => (\n <Field\n key={field.key}\n field={field}\n value={values[field.key]}\n error={fieldErrors[field.key]}\n onChange={(v) => setValue(field.key, v)}\n />\n ))}\n\n {form.honeypotField ? (\n <span aria-hidden=\"true\" style={{ position: \"absolute\", left: \"-9999px\" }}>\n <input\n type=\"text\"\n name={form.honeypotField}\n tabIndex={-1}\n autoComplete=\"off\"\n value={String(values[form.honeypotField] ?? \"\")}\n onChange={(e) => setValue(form.honeypotField as string, e.target.value)}\n />\n </span>\n ) : null}\n\n {showTurnstile ? <div className=\"cf-turnstile\" data-sitekey={turnstileSiteKey} /> : null}\n\n <button\n type=\"submit\"\n hidden={paged && step < last}\n disabled={status.state === \"submitting\"}\n data-bcms-form-part=\"submit\"\n >\n {status.state === \"submitting\" ? \"Submitting…\" : form.submitLabel ?? \"Submit\"}\n </button>\n\n {status.message ? (\n <p\n className={`bcms-form-status bcms-form-status--${status.state}`}\n role=\"status\"\n aria-live=\"polite\"\n >\n {status.message}\n </p>\n ) : null}\n </form>\n );\n}\n\ninterface FieldProps {\n field: DeliveryFormField;\n value: FormValue;\n error?: string;\n onChange: (value: FormValue) => void;\n}\n\nfunction Field({ field, value, error, onChange }: FieldProps): ReactElement {\n const required = field.required;\n const errorNode = error ? (\n <span className=\"bcms-field-error\" role=\"alert\">\n {error}\n </span>\n ) : null;\n // Every author-editable string carries a `data-bcms-form-part` so the Visual Editor can\n // repaint it live. A string the dock can edit but cannot mark shows stale on the canvas.\n const helpNode = field.helpText ? (\n <span className=\"bcms-field-help\" data-bcms-form-part=\"help\">\n {field.helpText}\n </span>\n ) : null;\n\n if (field.type === \"checkbox\" || field.type === \"consent\") {\n return (\n <label\n className=\"bcms-field bcms-field--checkbox\"\n data-field={field.key}\n data-bcms-form-field={field.key}\n >\n <input\n type=\"checkbox\"\n name={field.key}\n required={required}\n checked={Boolean(value)}\n onChange={(e) => onChange(e.target.checked)}\n />{\" \"}\n <span data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </span>\n {helpNode}\n {errorNode}\n </label>\n );\n }\n\n // Pick-one and pick-many are groups of controls, not a single element, so they\n // return their own <fieldset> rather than flowing into the label+control shape\n // below. Without these two branches both fell through to `<input type=\"text\">`.\n if (field.type === \"radio\" || field.type === \"checkboxes\") {\n const multi = field.type === \"checkboxes\";\n const selected = asStringArray(value);\n return (\n <fieldset\n className={`bcms-field bcms-field--${multi ? \"checkboxes\" : \"radio\"}`}\n data-field={field.key}\n data-bcms-form-field={field.key}\n >\n <legend data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </legend>\n {(field.options ?? []).map((opt) => (\n <label key={opt} className=\"bcms-option\">\n <input\n type={multi ? \"checkbox\" : \"radio\"}\n name={field.key}\n value={opt}\n // `required` on every radio in a group is the native way to demand one\n // choice. On checkboxes it would demand ALL of them, so it is omitted —\n // the API enforces non-empty instead.\n required={!multi && required}\n checked={multi ? selected.includes(opt) : value === opt}\n onChange={(e) =>\n onChange(multi ? toggleOption(value, opt, e.target.checked) : opt)\n }\n />{\" \"}\n <span data-bcms-form-part=\"option\">{opt}</span>\n </label>\n ))}\n {helpNode}\n {errorNode}\n </fieldset>\n );\n }\n\n const str = String(value ?? \"\");\n let control: ReactElement;\n if (field.type === \"textarea\") {\n control = (\n <textarea\n name={field.key}\n placeholder={field.placeholder}\n required={required}\n value={str}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n } else if (field.type === \"select\") {\n control = (\n <select name={field.key} required={required} value={str} onChange={(e) => onChange(e.target.value)}>\n <option value=\"\" disabled>\n Choose…\n </option>\n {(field.options ?? []).map((opt) => (\n <option key={opt} value={opt} data-bcms-form-part=\"option\">\n {opt}\n </option>\n ))}\n </select>\n );\n } else if (field.type === \"phone\" && field.ui?.countryPicker) {\n // The fallback is a usable tel input inside the SAME wrapper, so the host's `.bcms-phone`\n // rules apply before and after the chunk lands and the swap is not a layout surprise.\n control = (\n <Suspense\n fallback={\n <span className=\"bcms-phone\">\n <input type=\"tel\" name={field.key} placeholder={field.placeholder} required={required} value={str} onChange={(e) => onChange(e.target.value)} autoComplete=\"tel\" dir=\"ltr\" />\n </span>\n }\n >\n <PhoneField\n name={field.key}\n value={str}\n onChange={onChange}\n placeholder={field.placeholder}\n required={required}\n defaultCountry={field.ui.defaultCountry}\n />\n </Suspense>\n );\n } else {\n control = (\n <input\n type={INPUT_TYPES[field.type] ?? \"text\"}\n name={field.key}\n placeholder={field.placeholder}\n required={required}\n value={str}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n }\n\n return (\n <p className=\"bcms-field\" data-field={field.key} data-bcms-form-field={field.key}>\n <label data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </label>\n <br />\n {control}\n {helpNode}\n {errorNode}\n </p>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/form.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * <BcmsForm> — render a BetterCMS form natively in React, the headless way.\n *\n * Pass a form straight from your content snapshot (`readSnapshot().forms[n]`) or\n * the delivery API. It renders real inputs (no iframe), handles conditional\n * `showIf` fields, URL-query prefill, the honeypot, an optional Turnstile widget,\n * and submits via `@bettercms-ai/sdk`'s `submitForm`. All visible markup is\n * unstyled and class-driven, so the host site owns the look.\n */\n\nimport { lazy, Suspense, useEffect, useMemo, useRef, useState, type FormEvent, type ReactElement } from \"react\";\nimport {\n submitForm,\n shouldShowField,\n formInitialValues,\n asStringArray,\n toggleOption,\n type DeliveryForm,\n type DeliveryFormField,\n type FormValue,\n type FormValues,\n type FormSubmitError,\n} from \"@bettercms-ai/sdk/read\";\n\nconst TURNSTILE_SRC = \"https://challenges.cloudflare.com/turnstile/v0/api.js\";\n\n/**\n * The country-picker phone control, loaded only when a form actually has one.\n *\n * It carries libphonenumber's metadata (~80KB), which no form without a picker should pay\n * for. The import specifier must survive into `dist/form.js` rather than being inlined —\n * see the `external` entry in tsup.config.ts and the dist greps in the tests.\n */\nconst PhoneField = lazy(() => import(\"./phone-field.js\"));\n\n// Field type → native <input type>. Other types render their own element.\nconst INPUT_TYPES: Partial<Record<DeliveryFormField[\"type\"], string>> = {\n text: \"text\",\n email: \"email\",\n phone: \"tel\",\n url: \"url\",\n number: \"number\",\n date: \"date\",\n};\n\nexport interface BcmsFormProps {\n /** A form from your content snapshot or the delivery API. */\n form: DeliveryForm;\n /** API origin. Defaults to the SDK default (https://api.bettercms.ai). */\n apiBase?: string;\n /** Turnstile site key — needed to render the widget when `form.turnstileEnabled`. */\n turnstileSiteKey?: string;\n /** Called with the new submission id after a successful submit (before any redirect). */\n onSubmitted?: (submissionId: string) => void;\n /** Class for the <form> element; field wrappers/status use `bcms-*` classes. */\n className?: string;\n}\n\ntype Status = { state: \"idle\" | \"submitting\" | \"error\" | \"success\"; message?: string };\n\nfunction readPrefill(): Record<string, string> {\n if (typeof window === \"undefined\") return {};\n const out: Record<string, string> = {};\n new URLSearchParams(window.location.search).forEach((v, k) => {\n out[k] = v;\n });\n return out;\n}\n\ntype Page = { label: string | null; key: string | null; fields: DeliveryFormField[] };\n\n/**\n * Cut the flat field list into pages at its `step` dividers, or null when it has none. Fields\n * before the first divider are page 1; an empty page is dropped. The hosted renderer and\n * BcmsForm.astro carry the same rule: each package publishes on its own, so none of them can\n * count on a newer shared helper arriving with it.\n */\nfunction splitSteps(fields: DeliveryFormField[]): Page[] | null {\n if (!fields.some((f) => f.type === \"step\")) return null;\n const pages: Page[] = [];\n let cur: Page = { label: null, key: null, fields: [] };\n for (const f of fields) {\n if (f.type !== \"step\") {\n cur.fields.push(f);\n continue;\n }\n if (cur.fields.length) pages.push(cur);\n cur = { label: f.label, key: f.key, fields: [] };\n }\n if (cur.fields.length) pages.push(cur);\n return pages;\n}\n\nfunction resetTurnstile(): void {\n const t = (globalThis as { turnstile?: { reset: () => void } }).turnstile;\n if (t) {\n try {\n t.reset();\n } catch {\n /* widget not mounted yet */\n }\n }\n}\n\nexport function BcmsForm({\n form,\n apiBase,\n turnstileSiteKey,\n onSubmitted,\n className,\n}: BcmsFormProps): ReactElement {\n const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);\n const [values, setValues] = useState<FormValues>(() => {\n const init = formInitialValues(form, readPrefill());\n if (form.honeypotField) init[form.honeypotField] = \"\";\n return init;\n });\n const [status, setStatus] = useState<Status>({ state: \"idle\" });\n const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});\n\n // Steps. Until the component has mounted every page is shown, so server-rendered output (and a\n // visitor without JavaScript) gets one long form; `enhanced` then turns it into pages.\n const pages = useMemo(() => splitSteps(form.fields), [form.fields]);\n const [enhanced, setEnhanced] = useState(false);\n const [step, setStep] = useState(0);\n const pageRefs = useRef<(HTMLFieldSetElement | null)[]>([]);\n const headingRefs = useRef<(HTMLHeadingElement | null)[]>([]);\n const moveFocus = useRef(false);\n const last = pages ? pages.length - 1 : 0;\n const paged = Boolean(pages && enhanced);\n useEffect(() => setEnhanced(true), []);\n // Focus follows the visitor to the new page's heading, never on first paint.\n useEffect(() => {\n if (!moveFocus.current) return;\n moveFocus.current = false;\n headingRefs.current[step]?.focus();\n }, [step]);\n\n // The browser's own validity, for ONE page: the first control that fails reports its message.\n function pageInvalid(i: number): boolean {\n const controls = pageRefs.current[i]?.querySelectorAll<HTMLInputElement>(\"input,select,textarea\") ?? [];\n for (const c of controls) {\n if (!c.checkValidity()) {\n c.reportValidity();\n return true;\n }\n }\n return false;\n }\n function goTo(i: number): void {\n moveFocus.current = true;\n setStep(i);\n }\n function next(): void {\n if (!pageInvalid(step)) goTo(step + 1);\n }\n\n // Cloudflare auto-renders any `.cf-turnstile` element once its script is present.\n useEffect(() => {\n if (!showTurnstile || document.querySelector(`script[src=\"${TURNSTILE_SRC}\"]`)) return;\n const script = document.createElement(\"script\");\n script.src = TURNSTILE_SRC;\n script.async = true;\n script.defer = true;\n document.head.appendChild(script);\n }, [showTurnstile]);\n\n const visibleFields = useMemo(\n () => form.fields.filter((f) => f.type !== \"hidden\" && f.type !== \"step\" && shouldShowField(f, values)),\n [form.fields, values],\n );\n\n const setValue = (key: string, value: FormValue) =>\n setValues((prev) => ({ ...prev, [key]: value }));\n\n async function handleSubmit(e: FormEvent<HTMLFormElement>): Promise<void> {\n e.preventDefault();\n // Enter inside a field before the last page moves on rather than submitting half a form.\n if (paged && step < last) return next();\n setStatus({ state: \"submitting\" });\n setFieldErrors({});\n\n const tokenEl = e.currentTarget.querySelector<HTMLInputElement>(\n '[name=\"cf-turnstile-response\"]',\n );\n const turnstileToken = tokenEl?.value || undefined;\n\n // Drop conditionally-hidden fields so a hidden required field can't block submit;\n // keep hidden-type fields and the honeypot (the API classifies bots on it).\n const data: FormValues = {};\n for (const field of form.fields) {\n if (field.type === \"step\") continue; // a divider is never an answer\n if (field.type === \"hidden\" || shouldShowField(field, values)) {\n data[field.key] = values[field.key];\n }\n }\n if (form.honeypotField) data[form.honeypotField] = values[form.honeypotField] ?? \"\";\n\n try {\n const { id } = await submitForm({ formId: form.id, data, turnstileToken, baseUrl: apiBase });\n onSubmitted?.(id);\n if (form.redirectUrl) {\n window.location.href = form.redirectUrl;\n return;\n }\n const reset = formInitialValues(form);\n if (form.honeypotField) reset[form.honeypotField] = \"\";\n setValues(reset);\n setStep(0);\n setStatus({\n state: \"success\",\n message: form.successMessage ?? \"Thanks! Your submission was received.\",\n });\n resetTurnstile();\n } catch (err) {\n const e2 = err as FormSubmitError;\n if (e2.fieldErrors) {\n setFieldErrors(e2.fieldErrors);\n // The server found a problem on a page the visitor has left: take them back to it.\n const bad = pages?.findIndex((p) => p.fields.some((f) => e2.fieldErrors?.[f.key]));\n if (bad !== undefined && bad >= 0) goTo(bad);\n }\n setStatus({ state: \"error\", message: e2.message || \"Something went wrong. Please try again.\" });\n resetTurnstile();\n }\n }\n\n return (\n // `data-bcms-form` carries the id so the Visual Editor can find THIS form in the\n // preview frame and patch its copy live. Valued rather than bare: the runtime\n // selectors are `form[data-bcms-form]`, which a valued attribute still matches.\n <form className={className} onSubmit={handleSubmit} noValidate data-bcms-form={form.id}>\n {pages\n ? pages.map((page, i) => (\n <fieldset\n key={page.key ?? \"first\"}\n ref={(el) => {\n pageRefs.current[i] = el;\n }}\n className=\"bcms-step\"\n data-bcms-step={i + 1}\n hidden={paged && i !== step}\n aria-labelledby={`bcms-step-${form.id}-${i + 1}`}\n >\n <h3\n id={`bcms-step-${form.id}-${i + 1}`}\n className=\"bcms-step-title\"\n tabIndex={-1}\n ref={(el) => {\n headingRefs.current[i] = el;\n }}\n data-bcms-form-field={page.key ?? undefined}\n >\n <span className=\"bcms-step-count\">\n Step {i + 1} of {pages.length}\n </span>\n {page.label ? (\n <>\n {\" \"}\n <span data-bcms-form-part=\"label\">{page.label}</span>\n </>\n ) : null}\n </h3>\n {visibleFields\n .filter((f) => page.fields.includes(f))\n .map((field) => (\n <Field\n key={field.key}\n field={field}\n value={values[field.key]}\n error={fieldErrors[field.key]}\n onChange={(v) => setValue(field.key, v)}\n />\n ))}\n {paged ? (\n <div className=\"bcms-step-nav\">\n {i > 0 ? (\n <button type=\"button\" data-bcms-step-back onClick={() => goTo(i - 1)}>\n Back\n </button>\n ) : null}\n {i < last ? (\n <button type=\"button\" data-bcms-step-next onClick={next}>\n Next\n </button>\n ) : null}\n </div>\n ) : null}\n </fieldset>\n ))\n : visibleFields.map((field) => (\n <Field\n key={field.key}\n field={field}\n value={values[field.key]}\n error={fieldErrors[field.key]}\n onChange={(v) => setValue(field.key, v)}\n />\n ))}\n\n {form.honeypotField ? (\n <span aria-hidden=\"true\" style={{ position: \"absolute\", left: \"-9999px\" }}>\n <input\n type=\"text\"\n name={form.honeypotField}\n tabIndex={-1}\n autoComplete=\"off\"\n value={String(values[form.honeypotField] ?? \"\")}\n onChange={(e) => setValue(form.honeypotField as string, e.target.value)}\n />\n </span>\n ) : null}\n\n {showTurnstile ? <div className=\"cf-turnstile\" data-sitekey={turnstileSiteKey} /> : null}\n\n <button\n type=\"submit\"\n hidden={paged && step < last}\n disabled={status.state === \"submitting\"}\n data-bcms-form-part=\"submit\"\n >\n {status.state === \"submitting\" ? \"Submitting…\" : form.submitLabel ?? \"Submit\"}\n </button>\n\n {status.message ? (\n <p\n className={`bcms-form-status bcms-form-status--${status.state}`}\n role=\"status\"\n aria-live=\"polite\"\n >\n {status.message}\n </p>\n ) : null}\n </form>\n );\n}\n\ninterface FieldProps {\n field: DeliveryFormField;\n value: FormValue;\n error?: string;\n onChange: (value: FormValue) => void;\n}\n\nfunction Field({ field, value, error, onChange }: FieldProps): ReactElement {\n const required = field.required;\n const errorNode = error ? (\n <span className=\"bcms-field-error\" role=\"alert\">\n {error}\n </span>\n ) : null;\n // Every author-editable string carries a `data-bcms-form-part` so the Visual Editor can\n // repaint it live. A string the dock can edit but cannot mark shows stale on the canvas.\n const helpNode = field.helpText ? (\n <span className=\"bcms-field-help\" data-bcms-form-part=\"help\">\n {field.helpText}\n </span>\n ) : null;\n\n if (field.type === \"checkbox\" || field.type === \"consent\") {\n return (\n <label\n className=\"bcms-field bcms-field--checkbox\"\n data-field={field.key}\n data-bcms-form-field={field.key}\n >\n <input\n type=\"checkbox\"\n name={field.key}\n required={required}\n checked={Boolean(value)}\n onChange={(e) => onChange(e.target.checked)}\n />{\" \"}\n <span data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </span>\n {helpNode}\n {errorNode}\n </label>\n );\n }\n\n // Pick-one and pick-many are groups of controls, not a single element, so they\n // return their own <fieldset> rather than flowing into the label+control shape\n // below. Without these two branches both fell through to `<input type=\"text\">`.\n if (field.type === \"radio\" || field.type === \"checkboxes\") {\n const multi = field.type === \"checkboxes\";\n const selected = asStringArray(value);\n return (\n <fieldset\n className={`bcms-field bcms-field--${multi ? \"checkboxes\" : \"radio\"}`}\n data-field={field.key}\n data-bcms-form-field={field.key}\n >\n <legend data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </legend>\n {(field.options ?? []).map((opt) => (\n <label key={opt} className=\"bcms-option\">\n <input\n type={multi ? \"checkbox\" : \"radio\"}\n name={field.key}\n value={opt}\n // `required` on every radio in a group is the native way to demand one\n // choice. On checkboxes it would demand ALL of them, so it is omitted —\n // the API enforces non-empty instead.\n required={!multi && required}\n checked={multi ? selected.includes(opt) : value === opt}\n onChange={(e) =>\n onChange(multi ? toggleOption(value, opt, e.target.checked) : opt)\n }\n />{\" \"}\n <span data-bcms-form-part=\"option\">{opt}</span>\n </label>\n ))}\n {helpNode}\n {errorNode}\n </fieldset>\n );\n }\n\n const str = String(value ?? \"\");\n let control: ReactElement;\n if (field.type === \"textarea\") {\n control = (\n <textarea\n name={field.key}\n placeholder={field.placeholder}\n required={required}\n value={str}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n } else if (field.type === \"select\") {\n control = (\n <select name={field.key} required={required} value={str} onChange={(e) => onChange(e.target.value)}>\n <option value=\"\" disabled>\n Choose…\n </option>\n {(field.options ?? []).map((opt) => (\n <option key={opt} value={opt} data-bcms-form-part=\"option\">\n {opt}\n </option>\n ))}\n </select>\n );\n } else if (field.type === \"phone\" && field.ui?.countryPicker) {\n // The fallback is a usable tel input inside the SAME wrapper, so the host's `.bcms-phone`\n // rules apply before and after the chunk lands and the swap is not a layout surprise.\n control = (\n <Suspense\n fallback={\n <span className=\"bcms-phone\">\n <input type=\"tel\" name={field.key} placeholder={field.placeholder} required={required} value={str} onChange={(e) => onChange(e.target.value)} autoComplete=\"tel\" dir=\"ltr\" />\n </span>\n }\n >\n <PhoneField\n name={field.key}\n value={str}\n onChange={onChange}\n placeholder={field.placeholder}\n required={required}\n defaultCountry={field.ui.defaultCountry}\n />\n </Suspense>\n );\n } else {\n control = (\n <input\n type={INPUT_TYPES[field.type] ?? \"text\"}\n name={field.key}\n placeholder={field.placeholder}\n required={required}\n value={str}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n }\n\n return (\n <p className=\"bcms-field\" data-field={field.key} data-bcms-form-field={field.key}>\n <label data-bcms-form-part=\"label\">\n {field.label}\n {required ? \" *\" : \"\"}\n </label>\n <br />\n {control}\n {helpNode}\n {errorNode}\n </p>\n );\n}\n"],"mappings":";;;;AAYA,SAAS,MAAM,UAAU,WAAW,SAAS,QAAQ,gBAAmD;AACxG;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AAuOS,SAIE,UAEE,KANJ;AArOhB,IAAM,gBAAgB;AAStB,IAAM,aAAa,KAAK,MAAM,OAAO,kBAAkB,CAAC;AAGxD,IAAM,cAAkE;AAAA,EACtE,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AACR;AAiBA,SAAS,cAAsC;AAC7C,MAAI,OAAO,WAAW,YAAa,QAAO,CAAC;AAC3C,QAAM,MAA8B,CAAC;AACrC,MAAI,gBAAgB,OAAO,SAAS,MAAM,EAAE,QAAQ,CAAC,GAAG,MAAM;AAC5D,QAAI,CAAC,IAAI;AAAA,EACX,CAAC;AACD,SAAO;AACT;AAUA,SAAS,WAAW,QAA4C;AAC9D,MAAI,CAAC,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM,EAAG,QAAO;AACnD,QAAM,QAAgB,CAAC;AACvB,MAAI,MAAY,EAAE,OAAO,MAAM,KAAK,MAAM,QAAQ,CAAC,EAAE;AACrD,aAAW,KAAK,QAAQ;AACtB,QAAI,EAAE,SAAS,QAAQ;AACrB,UAAI,OAAO,KAAK,CAAC;AACjB;AAAA,IACF;AACA,QAAI,IAAI,OAAO,OAAQ,OAAM,KAAK,GAAG;AACrC,UAAM,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,KAAK,QAAQ,CAAC,EAAE;AAAA,EACjD;AACA,MAAI,IAAI,OAAO,OAAQ,OAAM,KAAK,GAAG;AACrC,SAAO;AACT;AAEA,SAAS,iBAAuB;AAC9B,QAAM,IAAK,WAAqD;AAChE,MAAI,GAAG;AACL,QAAI;AACF,QAAE,MAAM;AAAA,IACV,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAEO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,gBAAgB,QAAQ,KAAK,oBAAoB,gBAAgB;AACvE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAqB,MAAM;AACrD,UAAM,OAAO,kBAAkB,MAAM,YAAY,CAAC;AAClD,QAAI,KAAK,cAAe,MAAK,KAAK,aAAa,IAAI;AACnD,WAAO;AAAA,EACT,CAAC;AACD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAiB,EAAE,OAAO,OAAO,CAAC;AAC9D,QAAM,CAAC,aAAa,cAAc,IAAI,SAAiC,CAAC,CAAC;AAIzE,QAAM,QAAQ,QAAQ,MAAM,WAAW,KAAK,MAAM,GAAG,CAAC,KAAK,MAAM,CAAC;AAClE,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAClC,QAAM,WAAW,OAAuC,CAAC,CAAC;AAC1D,QAAM,cAAc,OAAsC,CAAC,CAAC;AAC5D,QAAM,YAAY,OAAO,KAAK;AAC9B,QAAM,OAAO,QAAQ,MAAM,SAAS,IAAI;AACxC,QAAM,QAAQ,QAAQ,SAAS,QAAQ;AACvC,YAAU,MAAM,YAAY,IAAI,GAAG,CAAC,CAAC;AAErC,YAAU,MAAM;AACd,QAAI,CAAC,UAAU,QAAS;AACxB,cAAU,UAAU;AACpB,gBAAY,QAAQ,IAAI,GAAG,MAAM;AAAA,EACnC,GAAG,CAAC,IAAI,CAAC;AAGT,WAAS,YAAY,GAAoB;AACvC,UAAM,WAAW,SAAS,QAAQ,CAAC,GAAG,iBAAmC,uBAAuB,KAAK,CAAC;AACtG,eAAW,KAAK,UAAU;AACxB,UAAI,CAAC,EAAE,cAAc,GAAG;AACtB,UAAE,eAAe;AACjB,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,WAAS,KAAK,GAAiB;AAC7B,cAAU,UAAU;AACpB,YAAQ,CAAC;AAAA,EACX;AACA,WAAS,OAAa;AACpB,QAAI,CAAC,YAAY,IAAI,EAAG,MAAK,OAAO,CAAC;AAAA,EACvC;AAGA,YAAU,MAAM;AACd,QAAI,CAAC,iBAAiB,SAAS,cAAc,eAAe,aAAa,IAAI,EAAG;AAChF,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,MAAM;AACb,WAAO,QAAQ;AACf,WAAO,QAAQ;AACf,aAAS,KAAK,YAAY,MAAM;AAAA,EAClC,GAAG,CAAC,aAAa,CAAC;AAElB,QAAM,gBAAgB;AAAA,IACpB,MAAM,KAAK,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,SAAS,UAAU,gBAAgB,GAAG,MAAM,CAAC;AAAA,IACtG,CAAC,KAAK,QAAQ,MAAM;AAAA,EACtB;AAEA,QAAM,WAAW,CAAC,KAAa,UAC7B,UAAU,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,GAAG,GAAG,MAAM,EAAE;AAEjD,iBAAe,aAAa,GAA8C;AACxE,MAAE,eAAe;AAEjB,QAAI,SAAS,OAAO,KAAM,QAAO,KAAK;AACtC,cAAU,EAAE,OAAO,aAAa,CAAC;AACjC,mBAAe,CAAC,CAAC;AAEjB,UAAM,UAAU,EAAE,cAAc;AAAA,MAC9B;AAAA,IACF;AACA,UAAM,iBAAiB,SAAS,SAAS;AAIzC,UAAM,OAAmB,CAAC;AAC1B,eAAW,SAAS,KAAK,QAAQ;AAC/B,UAAI,MAAM,SAAS,OAAQ;AAC3B,UAAI,MAAM,SAAS,YAAY,gBAAgB,OAAO,MAAM,GAAG;AAC7D,aAAK,MAAM,GAAG,IAAI,OAAO,MAAM,GAAG;AAAA,MACpC;AAAA,IACF;AACA,QAAI,KAAK,cAAe,MAAK,KAAK,aAAa,IAAI,OAAO,KAAK,aAAa,KAAK;AAEjF,QAAI;AACF,YAAM,EAAE,GAAG,IAAI,MAAM,WAAW,EAAE,QAAQ,KAAK,IAAI,MAAM,gBAAgB,SAAS,QAAQ,CAAC;AAC3F,oBAAc,EAAE;AAChB,UAAI,KAAK,aAAa;AACpB,eAAO,SAAS,OAAO,KAAK;AAC5B;AAAA,MACF;AACA,YAAM,QAAQ,kBAAkB,IAAI;AACpC,UAAI,KAAK,cAAe,OAAM,KAAK,aAAa,IAAI;AACpD,gBAAU,KAAK;AACf,cAAQ,CAAC;AACT,gBAAU;AAAA,QACR,OAAO;AAAA,QACP,SAAS,KAAK,kBAAkB;AAAA,MAClC,CAAC;AACD,qBAAe;AAAA,IACjB,SAAS,KAAK;AACZ,YAAM,KAAK;AACX,UAAI,GAAG,aAAa;AAClB,uBAAe,GAAG,WAAW;AAE7B,cAAM,MAAM,OAAO,UAAU,CAAC,MAAM,EAAE,OAAO,KAAK,CAAC,MAAM,GAAG,cAAc,EAAE,GAAG,CAAC,CAAC;AACjF,YAAI,QAAQ,UAAa,OAAO,EAAG,MAAK,GAAG;AAAA,MAC7C;AACA,gBAAU,EAAE,OAAO,SAAS,SAAS,GAAG,WAAW,0CAA0C,CAAC;AAC9F,qBAAe;AAAA,IACjB;AAAA,EACF;AAEA;AAAA;AAAA;AAAA;AAAA,IAIE,qBAAC,UAAK,WAAsB,UAAU,cAAc,YAAU,MAAC,kBAAgB,KAAK,IACjF;AAAA,cACG,MAAM,IAAI,CAAC,MAAM,MACf;AAAA,QAAC;AAAA;AAAA,UAEC,KAAK,CAAC,OAAO;AACX,qBAAS,QAAQ,CAAC,IAAI;AAAA,UACxB;AAAA,UACA,WAAU;AAAA,UACV,kBAAgB,IAAI;AAAA,UACpB,QAAQ,SAAS,MAAM;AAAA,UACvB,mBAAiB,aAAa,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,UAE9C;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,IAAI,aAAa,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,gBACjC,WAAU;AAAA,gBACV,UAAU;AAAA,gBACV,KAAK,CAAC,OAAO;AACX,8BAAY,QAAQ,CAAC,IAAI;AAAA,gBAC3B;AAAA,gBACA,wBAAsB,KAAK,OAAO;AAAA,gBAElC;AAAA,uCAAC,UAAK,WAAU,mBAAkB;AAAA;AAAA,oBAC1B,IAAI;AAAA,oBAAE;AAAA,oBAAK,MAAM;AAAA,qBACzB;AAAA,kBACC,KAAK,QACJ,iCACG;AAAA;AAAA,oBACD,oBAAC,UAAK,uBAAoB,SAAS,eAAK,OAAM;AAAA,qBAChD,IACE;AAAA;AAAA;AAAA,YACN;AAAA,YACC,cACE,OAAO,CAAC,MAAM,KAAK,OAAO,SAAS,CAAC,CAAC,EACrC,IAAI,CAAC,UACJ;AAAA,cAAC;AAAA;AAAA,gBAEC;AAAA,gBACA,OAAO,OAAO,MAAM,GAAG;AAAA,gBACvB,OAAO,YAAY,MAAM,GAAG;AAAA,gBAC5B,UAAU,CAAC,MAAM,SAAS,MAAM,KAAK,CAAC;AAAA;AAAA,cAJjC,MAAM;AAAA,YAKb,CACD;AAAA,YACF,QACC,qBAAC,SAAI,WAAU,iBACZ;AAAA,kBAAI,IACH,oBAAC,YAAO,MAAK,UAAS,uBAAmB,MAAC,SAAS,MAAM,KAAK,IAAI,CAAC,GAAG,kBAEtE,IACE;AAAA,cACH,IAAI,OACH,oBAAC,YAAO,MAAK,UAAS,uBAAmB,MAAC,SAAS,MAAM,kBAEzD,IACE;AAAA,eACN,IACE;AAAA;AAAA;AAAA,QApDC,KAAK,OAAO;AAAA,MAqDnB,CACD,IACD,cAAc,IAAI,CAAC,UACjB;AAAA,QAAC;AAAA;AAAA,UAEC;AAAA,UACA,OAAO,OAAO,MAAM,GAAG;AAAA,UACvB,OAAO,YAAY,MAAM,GAAG;AAAA,UAC5B,UAAU,CAAC,MAAM,SAAS,MAAM,KAAK,CAAC;AAAA;AAAA,QAJjC,MAAM;AAAA,MAKb,CACD;AAAA,MAEJ,KAAK,gBACJ,oBAAC,UAAK,eAAY,QAAO,OAAO,EAAE,UAAU,YAAY,MAAM,UAAU,GACtE;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,MAAM,KAAK;AAAA,UACX,UAAU;AAAA,UACV,cAAa;AAAA,UACb,OAAO,OAAO,OAAO,KAAK,aAAa,KAAK,EAAE;AAAA,UAC9C,UAAU,CAAC,MAAM,SAAS,KAAK,eAAyB,EAAE,OAAO,KAAK;AAAA;AAAA,MACxE,GACF,IACE;AAAA,MAEH,gBAAgB,oBAAC,SAAI,WAAU,gBAAe,gBAAc,kBAAkB,IAAK;AAAA,MAEpF;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,QAAQ,SAAS,OAAO;AAAA,UACxB,UAAU,OAAO,UAAU;AAAA,UAC3B,uBAAoB;AAAA,UAEnB,iBAAO,UAAU,eAAe,qBAAgB,KAAK,eAAe;AAAA;AAAA,MACvE;AAAA,MAEC,OAAO,UACN;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,sCAAsC,OAAO,KAAK;AAAA,UAC7D,MAAK;AAAA,UACL,aAAU;AAAA,UAET,iBAAO;AAAA;AAAA,MACV,IACE;AAAA,OACN;AAAA;AAEJ;AASA,SAAS,MAAM,EAAE,OAAO,OAAO,OAAO,SAAS,GAA6B;AAC1E,QAAM,WAAW,MAAM;AACvB,QAAM,YAAY,QAChB,oBAAC,UAAK,WAAU,oBAAmB,MAAK,SACrC,iBACH,IACE;AAGJ,QAAM,WAAW,MAAM,WACrB,oBAAC,UAAK,WAAU,mBAAkB,uBAAoB,QACnD,gBAAM,UACT,IACE;AAEJ,MAAI,MAAM,SAAS,cAAc,MAAM,SAAS,WAAW;AACzD,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,cAAY,MAAM;AAAA,QAClB,wBAAsB,MAAM;AAAA,QAE5B;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAM,MAAM;AAAA,cACZ;AAAA,cACA,SAAS,QAAQ,KAAK;AAAA,cACtB,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,OAAO;AAAA;AAAA,UAC5C;AAAA,UAAG;AAAA,UACH,qBAAC,UAAK,uBAAoB,SACvB;AAAA,kBAAM;AAAA,YACN,WAAW,OAAO;AAAA,aACrB;AAAA,UACC;AAAA,UACA;AAAA;AAAA;AAAA,IACH;AAAA,EAEJ;AAKA,MAAI,MAAM,SAAS,WAAW,MAAM,SAAS,cAAc;AACzD,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,WAAW,cAAc,KAAK;AACpC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,0BAA0B,QAAQ,eAAe,OAAO;AAAA,QACnE,cAAY,MAAM;AAAA,QAClB,wBAAsB,MAAM;AAAA,QAE5B;AAAA,+BAAC,YAAO,uBAAoB,SACzB;AAAA,kBAAM;AAAA,YACN,WAAW,OAAO;AAAA,aACrB;AAAA,WACE,MAAM,WAAW,CAAC,GAAG,IAAI,CAAC,QAC1B,qBAAC,WAAgB,WAAU,eACzB;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM,QAAQ,aAAa;AAAA,gBAC3B,MAAM,MAAM;AAAA,gBACZ,OAAO;AAAA,gBAIP,UAAU,CAAC,SAAS;AAAA,gBACpB,SAAS,QAAQ,SAAS,SAAS,GAAG,IAAI,UAAU;AAAA,gBACpD,UAAU,CAAC,MACT,SAAS,QAAQ,aAAa,OAAO,KAAK,EAAE,OAAO,OAAO,IAAI,GAAG;AAAA;AAAA,YAErE;AAAA,YAAG;AAAA,YACH,oBAAC,UAAK,uBAAoB,UAAU,eAAI;AAAA,eAd9B,GAeZ,CACD;AAAA,UACA;AAAA,UACA;AAAA;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,MAAM,OAAO,SAAS,EAAE;AAC9B,MAAI;AACJ,MAAI,MAAM,SAAS,YAAY;AAC7B,cACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,MAAM;AAAA,QACZ,aAAa,MAAM;AAAA,QACnB;AAAA,QACA,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA;AAAA,IAC1C;AAAA,EAEJ,WAAW,MAAM,SAAS,UAAU;AAClC,cACE,qBAAC,YAAO,MAAM,MAAM,KAAK,UAAoB,OAAO,KAAK,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAC/F;AAAA,0BAAC,YAAO,OAAM,IAAG,UAAQ,MAAC,0BAE1B;AAAA,OACE,MAAM,WAAW,CAAC,GAAG,IAAI,CAAC,QAC1B,oBAAC,YAAiB,OAAO,KAAK,uBAAoB,UAC/C,iBADU,GAEb,CACD;AAAA,OACH;AAAA,EAEJ,WAAW,MAAM,SAAS,WAAW,MAAM,IAAI,eAAe;AAG5D,cACE;AAAA,MAAC;AAAA;AAAA,QACC,UACE,oBAAC,UAAK,WAAU,cACd,8BAAC,WAAM,MAAK,OAAM,MAAM,MAAM,KAAK,aAAa,MAAM,aAAa,UAAoB,OAAO,KAAK,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAAG,cAAa,OAAM,KAAI,OAAM,GAC7K;AAAA,QAGF;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,MAAM;AAAA,YACZ,OAAO;AAAA,YACP;AAAA,YACA,aAAa,MAAM;AAAA,YACnB;AAAA,YACA,gBAAgB,MAAM,GAAG;AAAA;AAAA,QAC3B;AAAA;AAAA,IACF;AAAA,EAEJ,OAAO;AACL,cACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,YAAY,MAAM,IAAI,KAAK;AAAA,QACjC,MAAM,MAAM;AAAA,QACZ,aAAa,MAAM;AAAA,QACnB;AAAA,QACA,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA;AAAA,IAC1C;AAAA,EAEJ;AAEA,SACE,qBAAC,OAAE,WAAU,cAAa,cAAY,MAAM,KAAK,wBAAsB,MAAM,KAC3E;AAAA,yBAAC,WAAM,uBAAoB,SACxB;AAAA,YAAM;AAAA,MACN,WAAW,OAAO;AAAA,OACrB;AAAA,IACA,oBAAC,QAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,KACH;AAEJ;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { ResolvedSeo, SeoInput, resolveSeo } from '@bettercms-ai/sdk';
|
|
|
6
6
|
import { CSSProperties, ReactElement, ReactNode, ElementType, ImgHTMLAttributes } from 'react';
|
|
7
7
|
import { ImageSource, ImageFormat, ImageFit } from '@bettercms-ai/image-url';
|
|
8
8
|
export { ImageCrop, ImageFit, ImageFocal, ImageFormat, ImageSource, ImageUrlBuilder, ImageValueUrlOptions, imageUrl, default as imageUrlBuilder, imageValueObjectPosition, imageValueUrl } from '@bettercms-ai/image-url';
|
|
9
|
+
import { DeliveryForm as DeliveryForm$1 } from '@bettercms-ai/sdk/read';
|
|
9
10
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -154,7 +155,7 @@ interface BetterCMSNext<Schema extends Record<string, unknown>> {
|
|
|
154
155
|
tags?: string[];
|
|
155
156
|
}): Promise<DeliveredLayout | null>;
|
|
156
157
|
}
|
|
157
|
-
declare function createBetterCMS<Schema extends Record<string, unknown> = Record<string, unknown>>(
|
|
158
|
+
declare function createBetterCMS<Schema extends Record<string, unknown> = Record<string, unknown>>(options?: Partial<BetterCMSConfig>): BetterCMSNext<Schema>;
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
161
|
* buildMetadata — map a BetterCMS page's SEO onto a Next.js `Metadata` object.
|
|
@@ -563,7 +564,7 @@ declare function getComponent(idOrSlug: string, path?: string): DeliveryComponen
|
|
|
563
564
|
|
|
564
565
|
interface BcmsFormProps {
|
|
565
566
|
/** A form from your content snapshot or the delivery API. */
|
|
566
|
-
form: DeliveryForm;
|
|
567
|
+
form: DeliveryForm$1;
|
|
567
568
|
/** API origin. Defaults to the SDK default (https://api.bettercms.ai). */
|
|
568
569
|
apiBase?: string;
|
|
569
570
|
/** Turnstile site key — needed to render the widget when `form.turnstileEnabled`. */
|
|
@@ -835,7 +836,7 @@ declare function defineBetterCMSLive(config: BetterCMSLiveConfig): {
|
|
|
835
836
|
};
|
|
836
837
|
|
|
837
838
|
interface RevalidateRouteConfig {
|
|
838
|
-
/** Shared secret matching the project's revalidation webhook secret. */
|
|
839
|
+
/** Shared secret matching the project's revalidation webhook secret. Defaults to `BCMS_REVALIDATE_SECRET`. */
|
|
839
840
|
secret?: string;
|
|
840
841
|
}
|
|
841
842
|
declare function createRevalidateRoute(config?: RevalidateRouteConfig): {
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { resolveBcmsConfig } from "@bettercms-ai/sdk/config";
|
|
5
|
+
|
|
1
6
|
// src/types.ts
|
|
2
7
|
var BetterCMSError = class extends Error {
|
|
3
8
|
status;
|
|
@@ -31,7 +36,34 @@ function mapEntry(raw) {
|
|
|
31
36
|
updatedAt: raw.updatedAt ?? null
|
|
32
37
|
};
|
|
33
38
|
}
|
|
34
|
-
function
|
|
39
|
+
function configFromSite(config) {
|
|
40
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
41
|
+
const { config: site, missing } = resolveBcmsConfig({
|
|
42
|
+
options: { workspace: config.workspace, projectId: config.projectId },
|
|
43
|
+
env,
|
|
44
|
+
prefixes: ["NEXT_PUBLIC_", ""],
|
|
45
|
+
snapshot: () => {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(readFileSync(resolve(env.BCMS_CONTENT_FILE || "bcms-content.json"), "utf8"));
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (missing.includes("workspace")) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"[@bettercms-ai/next] No workspace: pass createBetterCMS({ workspace }), set NEXT_PUBLIC_BCMS_WORKSPACE, or run `bettercms pull-content` before the build so bcms-content.json is present."
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...config,
|
|
60
|
+
workspace: site.workspace,
|
|
61
|
+
projectId: site.projectId,
|
|
62
|
+
baseUrl: config.baseUrl ?? (site.apiUrl ? `${site.apiUrl}/api/v1/delivery` : void 0)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function createBetterCMS(options = {}) {
|
|
66
|
+
const config = configFromSite(options);
|
|
35
67
|
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
36
68
|
const previewBaseUrl = (config.previewBaseUrl ?? baseUrl.replace(/\/delivery$/, "/preview")).replace(/\/+$/, "");
|
|
37
69
|
const defaultRevalidate = config.revalidate ?? DEFAULT_REVALIDATE;
|
|
@@ -185,8 +217,8 @@ function createBetterCMS(config) {
|
|
|
185
217
|
import { resolveSeo } from "@bettercms-ai/sdk";
|
|
186
218
|
function withSiteUrlFallback(page, location) {
|
|
187
219
|
const siteUrl = location.siteUrl || (typeof process !== "undefined" ? process.env.BETTERCMS_SITE_URL : void 0);
|
|
188
|
-
const
|
|
189
|
-
return { siteUrl, path: location.path ||
|
|
220
|
+
const slugPath2 = page.slug ? page.slug === "home" ? "/" : `/${page.slug.replace(/^\/+/, "")}` : void 0;
|
|
221
|
+
return { siteUrl, path: location.path || slugPath2 };
|
|
190
222
|
}
|
|
191
223
|
var warnedNoCanonical = false;
|
|
192
224
|
function warnNoCanonical() {
|
|
@@ -274,8 +306,8 @@ function entryField(entry, path, opts) {
|
|
|
274
306
|
import { resolveSeo as resolveSeo2 } from "@bettercms-ai/sdk";
|
|
275
307
|
|
|
276
308
|
// src/snapshot.ts
|
|
277
|
-
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
278
|
-
import { dirname, resolve } from "path";
|
|
309
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
310
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
279
311
|
var SNAPSHOT_VERSION = 1;
|
|
280
312
|
var DEFAULT_SNAPSHOT_FILE = "bcms-content.json";
|
|
281
313
|
function serializeSnapshot(snapshot) {
|
|
@@ -321,9 +353,9 @@ function isPlatformSnapshot(parsed) {
|
|
|
321
353
|
return typeof snap?.$schema === "string" && snap.$schema.startsWith("bcms-content/") && typeof snap.collections === "object" && snap.collections !== null;
|
|
322
354
|
}
|
|
323
355
|
function writeSnapshot(path, snapshot) {
|
|
324
|
-
const out =
|
|
356
|
+
const out = resolve2(process.cwd(), path);
|
|
325
357
|
try {
|
|
326
|
-
if (isPlatformSnapshot(JSON.parse(
|
|
358
|
+
if (isPlatformSnapshot(JSON.parse(readFileSync2(out, "utf8")))) return false;
|
|
327
359
|
} catch {
|
|
328
360
|
}
|
|
329
361
|
mkdirSync(dirname(out), { recursive: true });
|
|
@@ -331,10 +363,10 @@ function writeSnapshot(path, snapshot) {
|
|
|
331
363
|
return true;
|
|
332
364
|
}
|
|
333
365
|
function readSnapshot(path) {
|
|
334
|
-
const out =
|
|
366
|
+
const out = resolve2(process.cwd(), path);
|
|
335
367
|
let json;
|
|
336
368
|
try {
|
|
337
|
-
json =
|
|
369
|
+
json = readFileSync2(out, "utf8");
|
|
338
370
|
} catch (err) {
|
|
339
371
|
if (err?.code === "ENOENT") return null;
|
|
340
372
|
throw err;
|
|
@@ -387,7 +419,7 @@ function getContent(config, opts = {}) {
|
|
|
387
419
|
listEntries: (model) => live.listEntries(model, { revalidate: false })
|
|
388
420
|
};
|
|
389
421
|
}
|
|
390
|
-
const resolved =
|
|
422
|
+
const resolved = resolve2(process.cwd(), path);
|
|
391
423
|
function load() {
|
|
392
424
|
const cached = snapshotCache.get(resolved);
|
|
393
425
|
if (cached) return cached;
|
|
@@ -523,22 +555,8 @@ async function BcmsDraftMarker() {
|
|
|
523
555
|
}
|
|
524
556
|
|
|
525
557
|
// src/draft-route.ts
|
|
558
|
+
import { previewTokenValid, safeRedirectPath } from "@bettercms-ai/sdk/draft";
|
|
526
559
|
var PREVIEW_TOKEN_COOKIE = "bcms-preview-token";
|
|
527
|
-
function safeRedirectPath(to) {
|
|
528
|
-
return to.startsWith("/") && !to.startsWith("//") ? to : "/";
|
|
529
|
-
}
|
|
530
|
-
function decodeTokenSlug(token) {
|
|
531
|
-
try {
|
|
532
|
-
const part = token.split(".")[1];
|
|
533
|
-
if (!part) return null;
|
|
534
|
-
const json = JSON.parse(
|
|
535
|
-
Buffer.from(part, "base64url").toString("utf8")
|
|
536
|
-
);
|
|
537
|
-
return json.entrySlug ?? null;
|
|
538
|
-
} catch {
|
|
539
|
-
return null;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
560
|
function createDraftModeRoute(config) {
|
|
543
561
|
const apiUrl = config.apiUrl.replace(/\/+$/, "");
|
|
544
562
|
async function GET(request) {
|
|
@@ -546,12 +564,7 @@ function createDraftModeRoute(config) {
|
|
|
546
564
|
const token = url.searchParams.get("token");
|
|
547
565
|
const redirectTo = url.searchParams.get("redirect") ?? "/";
|
|
548
566
|
if (!token) return new Response("Missing token", { status: 400 });
|
|
549
|
-
|
|
550
|
-
if (!slug) return new Response("Malformed token", { status: 400 });
|
|
551
|
-
const res = await fetch(
|
|
552
|
-
`${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}?token=${encodeURIComponent(token)}`
|
|
553
|
-
);
|
|
554
|
-
if (!res.ok) return new Response("Invalid or expired token", { status: 401 });
|
|
567
|
+
if (!await previewTokenValid(apiUrl, token)) return new Response("Invalid or expired token", { status: 401 });
|
|
555
568
|
const { draftMode, cookies } = await import("next/headers");
|
|
556
569
|
(await draftMode()).enable();
|
|
557
570
|
(await cookies()).set(PREVIEW_TOKEN_COOKIE, token, {
|
|
@@ -586,12 +599,12 @@ import {
|
|
|
586
599
|
} from "@bettercms-ai/image-url";
|
|
587
600
|
|
|
588
601
|
// src/forms-read.ts
|
|
589
|
-
import { readFileSync as
|
|
590
|
-
import { resolve as
|
|
602
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
603
|
+
import { resolve as resolve3 } from "path";
|
|
591
604
|
function readForms(path = process.env.BCMS_CONTENT_FILE || "bcms-content.json") {
|
|
592
605
|
let raw;
|
|
593
606
|
try {
|
|
594
|
-
raw =
|
|
607
|
+
raw = readFileSync3(resolve3(process.cwd(), path), "utf8");
|
|
595
608
|
} catch {
|
|
596
609
|
return { forms: [], turnstileSiteKey: null };
|
|
597
610
|
}
|
|
@@ -603,12 +616,12 @@ function getForm(idOrName, path) {
|
|
|
603
616
|
}
|
|
604
617
|
|
|
605
618
|
// src/pages-read.ts
|
|
606
|
-
import { readFileSync as
|
|
607
|
-
import { resolve as
|
|
619
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
620
|
+
import { resolve as resolve4 } from "path";
|
|
608
621
|
function readPages(path = process.env.BCMS_CONTENT_FILE || "bcms-content.json") {
|
|
609
622
|
let raw;
|
|
610
623
|
try {
|
|
611
|
-
raw =
|
|
624
|
+
raw = readFileSync4(resolve4(process.cwd(), path), "utf8");
|
|
612
625
|
} catch {
|
|
613
626
|
return [];
|
|
614
627
|
}
|
|
@@ -620,12 +633,12 @@ function getPage(slug, path) {
|
|
|
620
633
|
}
|
|
621
634
|
|
|
622
635
|
// src/components-read.ts
|
|
623
|
-
import { readFileSync as
|
|
624
|
-
import { resolve as
|
|
636
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
637
|
+
import { resolve as resolve5 } from "path";
|
|
625
638
|
function readComponents(path = process.env.BCMS_CONTENT_FILE || "bcms-content.json") {
|
|
626
639
|
let raw;
|
|
627
640
|
try {
|
|
628
|
-
raw =
|
|
641
|
+
raw = readFileSync5(resolve5(process.cwd(), path), "utf8");
|
|
629
642
|
} catch {
|
|
630
643
|
return [];
|
|
631
644
|
}
|
|
@@ -807,12 +820,18 @@ function safeEqual(a, b) {
|
|
|
807
820
|
const bb = Buffer.from(b);
|
|
808
821
|
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
809
822
|
}
|
|
823
|
+
function slugPath(slug) {
|
|
824
|
+
if (slug.startsWith("//") || /[\s?#\\]/.test(slug)) return null;
|
|
825
|
+
if (slug.startsWith("/")) return slug;
|
|
826
|
+
return slug === "home" || slug === "index" || slug === "" ? "/" : `/${slug}`;
|
|
827
|
+
}
|
|
810
828
|
function createRevalidateRoute(config = {}) {
|
|
811
829
|
async function POST(request) {
|
|
812
830
|
const body = await request.text();
|
|
813
|
-
|
|
831
|
+
const secret = config.secret ?? process.env.BCMS_REVALIDATE_SECRET;
|
|
832
|
+
if (secret) {
|
|
814
833
|
const provided = request.headers.get("x-bettercms-signature") ?? "";
|
|
815
|
-
const expected = `sha256=${createHmac("sha256",
|
|
834
|
+
const expected = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
816
835
|
if (!safeEqual(provided, expected)) {
|
|
817
836
|
return new Response("Invalid signature", { status: 401 });
|
|
818
837
|
}
|
|
@@ -823,10 +842,14 @@ function createRevalidateRoute(config = {}) {
|
|
|
823
842
|
} catch {
|
|
824
843
|
return new Response("Invalid JSON", { status: 400 });
|
|
825
844
|
}
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
845
|
+
const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
846
|
+
const tags = strings(payload.data?.tags);
|
|
847
|
+
const paths = [...new Set(strings(payload.data?.slugs).map(slugPath).filter((p) => p !== null))];
|
|
848
|
+
const { revalidateTag, revalidatePath } = await import("next/cache");
|
|
849
|
+
const tag = revalidateTag;
|
|
850
|
+
for (const t of tags) tag(t, { expire: 0 });
|
|
851
|
+
for (const p of paths) revalidatePath(p);
|
|
852
|
+
return Response.json({ revalidated: true, tags, paths });
|
|
830
853
|
}
|
|
831
854
|
return { POST };
|
|
832
855
|
}
|