@bettercms-ai/next 0.8.1 → 0.10.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 +9 -0
- package/dist/bettercms-snapshot.js.map +1 -1
- package/dist/blocks.js +28 -2
- package/dist/blocks.js.map +1 -1
- package/dist/index.d.ts +57 -10
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/live.d.ts +57 -9
- package/dist/live.js +61 -13
- package/dist/live.js.map +1 -1
- package/dist/visual-editing.d.ts +5 -1
- package/dist/visual-editing.js +8 -22
- package/dist/visual-editing.js.map +1 -1
- package/package.json +4 -4
package/dist/live.js
CHANGED
|
@@ -2,26 +2,74 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
// src/live.tsx
|
|
5
|
-
import { useEffect } from "react";
|
|
5
|
+
import { useEffect, useRef, useState } from "react";
|
|
6
6
|
import { useRouter } from "next/navigation";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
var pools = /* @__PURE__ */ new Map();
|
|
8
|
+
function subscribe(src, fn) {
|
|
9
|
+
let pool = pools.get(src);
|
|
10
|
+
if (!pool) {
|
|
10
11
|
const es = new EventSource(src);
|
|
12
|
+
pool = { es, listeners: /* @__PURE__ */ new Set() };
|
|
11
13
|
es.addEventListener("change", (e) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(e.data);
|
|
17
|
+
} catch {
|
|
18
|
+
return;
|
|
17
19
|
}
|
|
18
|
-
|
|
20
|
+
for (const l of pools.get(src)?.listeners ?? []) l(parsed);
|
|
19
21
|
});
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
+
pools.set(src, pool);
|
|
23
|
+
}
|
|
24
|
+
pool.listeners.add(fn);
|
|
25
|
+
return () => {
|
|
26
|
+
const p = pools.get(src);
|
|
27
|
+
if (!p) return;
|
|
28
|
+
p.listeners.delete(fn);
|
|
29
|
+
if (p.listeners.size === 0) {
|
|
30
|
+
p.es.close();
|
|
31
|
+
pools.delete(src);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function useBcmsLive(src, onChange) {
|
|
36
|
+
const cb = useRef(onChange);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
cb.current = onChange;
|
|
39
|
+
});
|
|
40
|
+
useEffect(() => subscribe(src, (e) => cb.current(e)), [src]);
|
|
41
|
+
}
|
|
42
|
+
function BcmsLive({ src, refreshOnChange = true, tags, onChange }) {
|
|
43
|
+
const router = useRouter();
|
|
44
|
+
useBcmsLive(src, (event) => {
|
|
45
|
+
if (tags?.length && !event.tags.some((t) => tags.includes(t))) return;
|
|
46
|
+
onChange?.(event);
|
|
47
|
+
if (refreshOnChange) router.refresh();
|
|
48
|
+
});
|
|
22
49
|
return null;
|
|
23
50
|
}
|
|
51
|
+
function useBcmsLiveEntry(initial, opts) {
|
|
52
|
+
const { src, slug, type = "entry", onStale } = opts;
|
|
53
|
+
const [value, setValue] = useState(initial);
|
|
54
|
+
const seeded = useRef(initial);
|
|
55
|
+
if (seeded.current !== initial) {
|
|
56
|
+
seeded.current = initial;
|
|
57
|
+
setValue(initial);
|
|
58
|
+
}
|
|
59
|
+
const stale = useRef(onStale);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
stale.current = onStale;
|
|
62
|
+
});
|
|
63
|
+
useBcmsLive(src, (event) => {
|
|
64
|
+
if (event.slug !== slug || event.type !== type) return;
|
|
65
|
+
if (event.doc !== void 0) setValue(event.doc);
|
|
66
|
+
else stale.current?.(event);
|
|
67
|
+
});
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
24
70
|
export {
|
|
25
|
-
BcmsLive
|
|
71
|
+
BcmsLive,
|
|
72
|
+
useBcmsLive,
|
|
73
|
+
useBcmsLiveEntry
|
|
26
74
|
};
|
|
27
75
|
//# sourceMappingURL=live.js.map
|
package/dist/live.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/live.tsx"],"sourcesContent":["\"use client\";\n/**\n *
|
|
1
|
+
{"version":3,"sources":["../src/live.tsx"],"sourcesContent":["\"use client\";\n/**\n * Live content for the App Router — the client half of the delivery SSE lane.\n *\n * The stream pushes the CHANGED DOCUMENT, not just a pointer to it (see the\n * backend's routes/delivery/live.ts), which is what makes the two exports here\n * differ in cost:\n *\n * <BcmsLive> refetches — `router.refresh()`, one RSC round trip.\n * useBcmsLiveEntry() patches — swaps in the pushed doc, ZERO round trips.\n *\n * Prefer the hook on anything that updates often. Keep `<BcmsLive>` for pages\n * rendered entirely on the server, where there is no client state to patch.\n *\n * import { BcmsLive, useBcmsLiveEntry } from \"@bettercms-ai/next/live\";\n * import { defineBetterCMSLive } from \"@bettercms-ai/next\";\n * const { liveSrc } = defineBetterCMSLive({ apiUrl, workspace, apiKey });\n * // in layout.tsx: <BcmsLive src={liveSrc} />\n *\n * `src` embeds a content:read key as a query param because browser EventSource\n * cannot send headers — use a read-only key.\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\n\n/** A `change` frame off the live stream. */\nexport interface BcmsChangeEvent<T = unknown> {\n type: \"entry\" | \"page\";\n slug: string;\n reason: \"publish\" | \"unpublish\" | \"draft\";\n /** Cache tags, `bcms:{ws}:all` + `bcms:{ws}:{type}:{slug}`. */\n tags: string[];\n projectId: string | null;\n /** When the change was committed. */\n at: string;\n /** When the server flushed this frame — `at`→`sentAt` is its own cost. */\n sentAt?: string;\n /**\n * The finished delivery payload. Absent on `unpublish`, and when the server\n * could not hydrate it (over quota, deleted, larger than the frame cap) — in\n * which case the client has to refetch, which is what it always used to do.\n */\n doc?: T;\n}\n\n// ── Shared connections ───────────────────────────────────────────────────────\n// One EventSource per URL, refcounted, because HTTP/1.1 browsers cap ~6 sockets\n// per host: a page with a handful of live components would otherwise spend its\n// whole connection budget on this stream and stall its own image loads.\n\ntype Listener = (e: BcmsChangeEvent) => void;\nconst pools = new Map<string, { es: EventSource; listeners: Set<Listener> }>();\n\nfunction subscribe(src: string, fn: Listener): () => void {\n let pool = pools.get(src);\n if (!pool) {\n const es = new EventSource(src);\n pool = { es, listeners: new Set() };\n es.addEventListener(\"change\", (e) => {\n let parsed: BcmsChangeEvent;\n try {\n parsed = JSON.parse((e as MessageEvent).data) as BcmsChangeEvent;\n } catch {\n return; // malformed frame — ignore, the next one will do\n }\n for (const l of pools.get(src)?.listeners ?? []) l(parsed);\n });\n pools.set(src, pool);\n }\n pool.listeners.add(fn);\n return () => {\n const p = pools.get(src);\n if (!p) return;\n p.listeners.delete(fn);\n if (p.listeners.size === 0) {\n p.es.close();\n pools.delete(src);\n }\n };\n}\n\n/** Raw subscription. `onChange` may change identity freely — it is read via a ref. */\nexport function useBcmsLive(src: string, onChange: (e: BcmsChangeEvent) => void): void {\n const cb = useRef(onChange);\n // Kept current in an effect, not during render: this hook's callers pass inline\n // arrows, and a render-phase write is the one thing that misbehaves under\n // StrictMode double-rendering. Declared first, so it lands before the subscribe.\n useEffect(() => {\n cb.current = onChange;\n });\n useEffect(() => subscribe(src, (e) => cb.current(e)), [src]);\n}\n\nexport interface BcmsLiveProps {\n /** Live SSE URL, e.g. from `defineBetterCMSLive().liveSrc`. */\n src: string;\n /** Refresh the route on each matching change. Default true. */\n refreshOnChange?: boolean;\n /**\n * Only react to changes touching one of these tags. Omit to react to\n * everything (the old behaviour). A blog post page passing its own tag stops\n * re-rendering every time an unrelated entry elsewhere is published.\n */\n tags?: string[];\n /** Called for every matching change, with the pushed `doc` when there is one. */\n onChange?: (event: BcmsChangeEvent) => void;\n}\n\nexport function BcmsLive({ src, refreshOnChange = true, tags, onChange }: BcmsLiveProps) {\n const router = useRouter();\n useBcmsLive(src, (event) => {\n if (tags?.length && !event.tags.some((t) => tags.includes(t))) return;\n onChange?.(event);\n if (refreshOnChange) router.refresh();\n });\n return null;\n}\n\nexport interface UseBcmsLiveEntryOptions {\n /** Live SSE URL, e.g. from `defineBetterCMSLive().liveSrc`. */\n src: string;\n /** The slug this component renders. */\n slug: string;\n /** Default \"entry\". */\n type?: \"entry\" | \"page\";\n /**\n * What to do when the resource is unpublished, or when the server pushed no\n * doc and a refetch is the only way forward. Default: leave the last value in\n * place (a live site keeps showing the last good render).\n */\n onStale?: (event: BcmsChangeEvent) => void;\n}\n\n/**\n * Server-render once, stay live for free.\n *\n * Seed it with the entry you already fetched on the server; it returns that\n * value until a matching change arrives, then returns the pushed document.\n * No refetch, no route re-render — the update costs one repaint.\n *\n * const post = useBcmsLiveEntry(initialPost, { src: liveSrc, slug: params.slug });\n * return <article>{post.data.title}</article>;\n */\nexport function useBcmsLiveEntry<T>(initial: T, opts: UseBcmsLiveEntryOptions): T {\n const { src, slug, type = \"entry\", onStale } = opts;\n const [value, setValue] = useState<T>(initial);\n\n // A navigation to a different slug re-renders with a new `initial`; without this\n // the hook would keep serving the previous route's document.\n const seeded = useRef(initial);\n if (seeded.current !== initial) {\n seeded.current = initial;\n setValue(initial);\n }\n\n const stale = useRef(onStale);\n useEffect(() => {\n stale.current = onStale;\n });\n\n useBcmsLive(src, (event) => {\n if (event.slug !== slug || event.type !== type) return;\n if (event.doc !== undefined) setValue(event.doc as T);\n else stale.current?.(event);\n });\n\n return value;\n}\n"],"mappings":";;;;AAsBA,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,iBAAiB;AA4B1B,IAAM,QAAQ,oBAAI,IAA2D;AAE7E,SAAS,UAAU,KAAa,IAA0B;AACxD,MAAI,OAAO,MAAM,IAAI,GAAG;AACxB,MAAI,CAAC,MAAM;AACT,UAAM,KAAK,IAAI,YAAY,GAAG;AAC9B,WAAO,EAAE,IAAI,WAAW,oBAAI,IAAI,EAAE;AAClC,OAAG,iBAAiB,UAAU,CAAC,MAAM;AACnC,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAO,EAAmB,IAAI;AAAA,MAC9C,QAAQ;AACN;AAAA,MACF;AACA,iBAAW,KAAK,MAAM,IAAI,GAAG,GAAG,aAAa,CAAC,EAAG,GAAE,MAAM;AAAA,IAC3D,CAAC;AACD,UAAM,IAAI,KAAK,IAAI;AAAA,EACrB;AACA,OAAK,UAAU,IAAI,EAAE;AACrB,SAAO,MAAM;AACX,UAAM,IAAI,MAAM,IAAI,GAAG;AACvB,QAAI,CAAC,EAAG;AACR,MAAE,UAAU,OAAO,EAAE;AACrB,QAAI,EAAE,UAAU,SAAS,GAAG;AAC1B,QAAE,GAAG,MAAM;AACX,YAAM,OAAO,GAAG;AAAA,IAClB;AAAA,EACF;AACF;AAGO,SAAS,YAAY,KAAa,UAA8C;AACrF,QAAM,KAAK,OAAO,QAAQ;AAI1B,YAAU,MAAM;AACd,OAAG,UAAU;AAAA,EACf,CAAC;AACD,YAAU,MAAM,UAAU,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;AAC7D;AAiBO,SAAS,SAAS,EAAE,KAAK,kBAAkB,MAAM,MAAM,SAAS,GAAkB;AACvF,QAAM,SAAS,UAAU;AACzB,cAAY,KAAK,CAAC,UAAU;AAC1B,QAAI,MAAM,UAAU,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,EAAG;AAC/D,eAAW,KAAK;AAChB,QAAI,gBAAiB,QAAO,QAAQ;AAAA,EACtC,CAAC;AACD,SAAO;AACT;AA2BO,SAAS,iBAAoB,SAAY,MAAkC;AAChF,QAAM,EAAE,KAAK,MAAM,OAAO,SAAS,QAAQ,IAAI;AAC/C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAY,OAAO;AAI7C,QAAM,SAAS,OAAO,OAAO;AAC7B,MAAI,OAAO,YAAY,SAAS;AAC9B,WAAO,UAAU;AACjB,aAAS,OAAO;AAAA,EAClB;AAEA,QAAM,QAAQ,OAAO,OAAO;AAC5B,YAAU,MAAM;AACd,UAAM,UAAU;AAAA,EAClB,CAAC;AAED,cAAY,KAAK,CAAC,UAAU;AAC1B,QAAI,MAAM,SAAS,QAAQ,MAAM,SAAS,KAAM;AAChD,QAAI,MAAM,QAAQ,OAAW,UAAS,MAAM,GAAQ;AAAA,QAC/C,OAAM,UAAU,KAAK;AAAA,EAC5B,CAAC;AAED,SAAO;AACT;","names":[]}
|
package/dist/visual-editing.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
interface VisualEditingProps {
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Studio origin, for the deep links and for the script itself. Defaults to
|
|
4
|
+
* `https://demo.bettercms.ai` — the same origin the dashboard hands out as
|
|
5
|
+
* `NEXT_PUBLIC_BCMS_STUDIO_URL`. (`dashboard.bettercms.ai` resolves but 404s.)
|
|
6
|
+
*/
|
|
3
7
|
studioUrl?: string;
|
|
4
8
|
}
|
|
5
9
|
declare function VisualEditing({ studioUrl }: VisualEditingProps): null;
|
package/dist/visual-editing.js
CHANGED
|
@@ -3,29 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
// src/visual-editing.tsx
|
|
5
5
|
import { useEffect } from "react";
|
|
6
|
-
|
|
7
|
-
function editHref(studioUrl, p) {
|
|
8
|
-
const base = studioUrl.replace(/\/+$/, "");
|
|
9
|
-
const section = p.type === "page" ? "pages" : "content";
|
|
10
|
-
return `${base}/${section}/${encodeURIComponent(p.id)}?field=${encodeURIComponent(p.field)}`;
|
|
11
|
-
}
|
|
12
|
-
function VisualEditing({ studioUrl = "https://dashboard.bettercms.ai" }) {
|
|
6
|
+
function VisualEditing({ studioUrl = "https://demo.bettercms.ai" }) {
|
|
13
7
|
useEffect(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
window.open(editHref(studioUrl, decoded.payload), "_blank", "noopener");
|
|
22
|
-
}
|
|
23
|
-
document.addEventListener("click", onClick, true);
|
|
24
|
-
document.documentElement.dataset.bcmsVisualEditing = "on";
|
|
25
|
-
return () => {
|
|
26
|
-
document.removeEventListener("click", onClick, true);
|
|
27
|
-
delete document.documentElement.dataset.bcmsVisualEditing;
|
|
28
|
-
};
|
|
8
|
+
const src = `${studioUrl.replace(/\/+$/, "")}/bcms-visual-editing.js`;
|
|
9
|
+
if (document.querySelector(`script[src="${src}"]`)) return;
|
|
10
|
+
const el = document.createElement("script");
|
|
11
|
+
el.src = src;
|
|
12
|
+
el.defer = true;
|
|
13
|
+
el.dataset.bcmsStudio = studioUrl;
|
|
14
|
+
document.head.appendChild(el);
|
|
29
15
|
}, [studioUrl]);
|
|
30
16
|
return null;
|
|
31
17
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/visual-editing.tsx"],"sourcesContent":["\"use client\";\n/**\n * <VisualEditing> — click-to-edit overlay for draft preview.
|
|
1
|
+
{"version":3,"sources":["../src/visual-editing.tsx"],"sourcesContent":["\"use client\";\n/**\n * <VisualEditing> — click-to-edit overlay for draft preview. Alt+Click any value that\n * carries stega provenance to open the matching dashboard editor on that field.\n * Render it only in draft mode.\n *\n * import { VisualEditing } from \"@bettercms-ai/next/visual-editing\";\n * {isDraft && <VisualEditing studioUrl=\"https://demo.bettercms.ai\" />}\n *\n * The decoder is NOT bundled here. It is one script served from the dashboard origin\n * (`/bcms-visual-editing.js`), shared with @bettercms-ai/astro and with plain-HTML sites\n * that install it by hand — a customer build is un-upgradable, so a copy baked into one\n * could never be fixed. Requires the read client to be in draft perspective with\n * `stega: true`; on a published build there is no provenance and the script is inert.\n */\nimport { useEffect } from \"react\";\n\nexport interface VisualEditingProps {\n /**\n * Studio origin, for the deep links and for the script itself. Defaults to\n * `https://demo.bettercms.ai` — the same origin the dashboard hands out as\n * `NEXT_PUBLIC_BCMS_STUDIO_URL`. (`dashboard.bettercms.ai` resolves but 404s.)\n */\n studioUrl?: string;\n}\n\nexport function VisualEditing({ studioUrl = \"https://demo.bettercms.ai\" }: VisualEditingProps) {\n useEffect(() => {\n const src = `${studioUrl.replace(/\\/+$/, \"\")}/bcms-visual-editing.js`;\n if (document.querySelector(`script[src=\"${src}\"]`)) return;\n const el = document.createElement(\"script\");\n el.src = src;\n el.defer = true;\n el.dataset.bcmsStudio = studioUrl;\n document.head.appendChild(el);\n // No cleanup: removing the tag would not detach the listeners the script installed,\n // so a remove-and-remount would leave one set orphaned and add a second. The tag is\n // its own guard instead — this component is mounted once, in a draft-mode layout.\n }, [studioUrl]);\n return null;\n}\n"],"mappings":";;;;AAeA,SAAS,iBAAiB;AAWnB,SAAS,cAAc,EAAE,YAAY,4BAA4B,GAAuB;AAC7F,YAAU,MAAM;AACd,UAAM,MAAM,GAAG,UAAU,QAAQ,QAAQ,EAAE,CAAC;AAC5C,QAAI,SAAS,cAAc,eAAe,GAAG,IAAI,EAAG;AACpD,UAAM,KAAK,SAAS,cAAc,QAAQ;AAC1C,OAAG,MAAM;AACT,OAAG,QAAQ;AACX,OAAG,QAAQ,aAAa;AACxB,aAAS,KAAK,YAAY,EAAE;AAAA,EAI9B,GAAG,CAAC,SAAS,CAAC;AACd,SAAO;AACT;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettercms-ai/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "The BetterCMS adapter for Next.js
|
|
5
|
+
"description": "The BetterCMS adapter for Next.js — typed, cache-aware content reads (getEntry/listEntries), draft preview, build-time content snapshot, and llms.txt generation for the App Router. The /blocks, /form, /field, /search and /visual-editing entries are framework-free React and work in any React app (see the React+TS starter), which is why `next` is an OPTIONAL peer.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"bettercms-snapshot": "./dist/bettercms-snapshot.js"
|
|
8
8
|
},
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@bettercms-ai/image-url": "^0.1.0",
|
|
57
|
-
"@bettercms-ai/sdk": "^1.
|
|
58
|
-
"@bettercms-ai/types": "^1.
|
|
57
|
+
"@bettercms-ai/sdk": "^1.12.0",
|
|
58
|
+
"@bettercms-ai/types": "^1.8.0",
|
|
59
59
|
"@bettercms-ai/ui": "^0.4.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|