@avocadostudio-ai/site-sdk 0.3.0 → 0.3.2
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 +121 -8
- package/dist/editor-overlay.d.ts +17 -0
- package/dist/editor-overlay.js +17 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -169,10 +169,23 @@ none of them.
|
|
|
169
169
|
| `.../navigation` | `buildNavItems`, `buildSiteHeaderBlock` |
|
|
170
170
|
| `.../seo` | `buildPageMetadata` and the title/description derivation |
|
|
171
171
|
|
|
172
|
-
`useLivePreviewBlocks`
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
`useLivePreviewBlocks` is not here. It lives in
|
|
173
|
+
`@avocadostudio-ai/preview-adapter`, which you must depend on directly to import
|
|
174
|
+
from it (`LivePreviewProvider` itself is re-exported above, the hook is not).
|
|
175
|
+
|
|
176
|
+
It is worth being precise about what it is for, because an earlier version of
|
|
177
|
+
this paragraph was not. Rendering a live draft through **your own** components
|
|
178
|
+
does not require it — that is the default, and it is what an existing site
|
|
179
|
+
should do: skip `createSitePage`, skip `@avocadostudio-ai/blocks/styles.css`,
|
|
180
|
+
render the draft page with your own components, and mount `EditorOverlay`.
|
|
181
|
+
Streamed edits reach the page as DOM writes keyed on `data-editable-target`.
|
|
182
|
+
|
|
183
|
+
What the hook buys is having those edits arrive as **React state** instead, so
|
|
184
|
+
components that own their own markup are not fighting an `innerHTML` write. It
|
|
185
|
+
is the newer and less exercised of the two paths, and it re-renders on the
|
|
186
|
+
client — anything resolved from a server-only registry blanks. Take the default
|
|
187
|
+
path first. See
|
|
188
|
+
[Existing sites keep their own components](https://docs.avocadostudio.dev/integration/nextjs-integration#existing-sites-keep-their-own-components).
|
|
176
189
|
|
|
177
190
|
## API Contract
|
|
178
191
|
|
|
@@ -213,6 +226,29 @@ site's own origin** for `/api/editor/blocks` and `/api/editor/pages`. Mount only
|
|
|
213
226
|
the orchestrator and the editor loads, connects, and answers four requests with
|
|
214
227
|
`net::ERR_FAILED` in a console you have to open to see.
|
|
215
228
|
|
|
229
|
+
### And then run the editor
|
|
230
|
+
|
|
231
|
+
The two handlers above are the whole backend. The editor UI itself is a
|
|
232
|
+
prebuilt SPA that ships inside `@avocadostudio-ai/cli`, so there is nothing to
|
|
233
|
+
clone and no third service to deploy:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npx @avocadostudio-ai/cli start \
|
|
237
|
+
--orchestrator http://localhost:3000/api/avocado \
|
|
238
|
+
--preview http://localhost:3000
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
That serves the editor on `http://localhost:4100`, pointed at the orchestrator
|
|
242
|
+
you just mounted. `--preview` is the origin the editor frames, so it is your
|
|
243
|
+
site's dev server — **your** port, which is only 3000 if that is where your
|
|
244
|
+
`dev` script runs.
|
|
245
|
+
|
|
246
|
+
This line is here because the composition is not obvious from either package on
|
|
247
|
+
its own: the CLI's own README says it does not bundle an orchestrator and
|
|
248
|
+
points at one you deploy separately, which is true and reads as "you need a
|
|
249
|
+
third service". You do not. `createOrchestrator` above *is* the orchestrator,
|
|
250
|
+
and the CLI is the other half of it.
|
|
251
|
+
|
|
216
252
|
## The adapter contract
|
|
217
253
|
|
|
218
254
|
`createOrchestrator({ adapter })` is the whole of library mode: SQLite is the
|
|
@@ -266,6 +302,13 @@ the projection back destroys what it was projected from. `context.published` is
|
|
|
266
302
|
the baseline; `undefined` means *no baseline available*, never *the site was
|
|
267
303
|
empty*. See [Publishing back to a real CMS](#publishing-back-to-a-real-cms).
|
|
268
304
|
|
|
305
|
+
It survives a process restart: a restart reloads the draft from SQLite without
|
|
306
|
+
re-seeding it, and the baseline used to be dropped on that path, so a publisher
|
|
307
|
+
written the way this section recommends — diff against the baseline, refuse when
|
|
308
|
+
there is none — could never publish again afterwards. The orchestrator now
|
|
309
|
+
re-reads the adapter for the baseline alone in that case. It can still be
|
|
310
|
+
`undefined` if that read fails, so keep the refusal.
|
|
311
|
+
|
|
269
312
|
`CmsAdapter`, `CmsCapabilities`, `CmsPublishContext`, `CmsPerspective` and
|
|
270
313
|
`CreateOrchestratorConfig` are all exported from
|
|
271
314
|
`@avocadostudio-ai/site-sdk/server`.
|
|
@@ -559,10 +602,15 @@ that guesses writes a URL where a reference belongs.
|
|
|
559
602
|
|
|
560
603
|
If you did not embed your own source snapshot in block props, `onPublish`'s
|
|
561
604
|
third argument carries `context.published` — the pages as your adapter last
|
|
562
|
-
reported them. It is the copy taken at bootstrap, not a fresh read,
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
605
|
+
reported them. It is the copy taken at bootstrap, not a fresh read, and it
|
|
606
|
+
survives a restart that reloads the draft from storage: the orchestrator
|
|
607
|
+
re-reads it on the next request, which is sound because nothing was published in
|
|
608
|
+
between, so the CMS still holds exactly the content the lost copy was a copy of.
|
|
609
|
+
|
|
610
|
+
It can still be `undefined` — the adapter read can fail, and baselines are
|
|
611
|
+
evicted once enough sessions accumulate. Treat that as "no baseline available",
|
|
612
|
+
never as "the site was empty": publishing every field on that assumption is the
|
|
613
|
+
overwrite all of this exists to prevent.
|
|
566
614
|
|
|
567
615
|
## Registering your own block schemas
|
|
568
616
|
|
|
@@ -674,6 +722,71 @@ Declaring a type you never registered is not silently dropped: it has no schema
|
|
|
674
722
|
to describe, so it cannot reach the manifest, and the orchestrator logs a warning
|
|
675
723
|
naming it the first time the manifest is served.
|
|
676
724
|
|
|
725
|
+
## Mounting the overlay
|
|
726
|
+
|
|
727
|
+
Nothing on the page is clickable until `EditorOverlay` is on it. It is the whole
|
|
728
|
+
of the selection UI — the block outline, the type badge, the field pills, the
|
|
729
|
+
postMessage channel back to the editor — and a preview route without it renders
|
|
730
|
+
correctly, frames correctly, and does not respond to a click.
|
|
731
|
+
|
|
732
|
+
It takes two required props, and the second one is not guessable:
|
|
733
|
+
|
|
734
|
+
```tsx
|
|
735
|
+
// app/preview-draft/[[...slug]]/page.tsx
|
|
736
|
+
import { resolveEditorContext } from "@avocadostudio-ai/site-sdk/draft"
|
|
737
|
+
import { EditorOverlay } from "@avocadostudio-ai/site-sdk/editor"
|
|
738
|
+
import { buildSlug } from "@avocadostudio-ai/site-sdk"
|
|
739
|
+
|
|
740
|
+
export default async function PreviewPage({ params, searchParams }) {
|
|
741
|
+
const { slug } = await params
|
|
742
|
+
const ctx = await resolveEditorContext(await searchParams)
|
|
743
|
+
const path = buildSlug(slug) // ["fr","pricing"] -> "/fr/pricing"
|
|
744
|
+
|
|
745
|
+
return (
|
|
746
|
+
<>
|
|
747
|
+
<MyPage slug={path} />
|
|
748
|
+
{ctx && <EditorOverlay slug={path} editorOrigin={ctx.editorOrigin} />}
|
|
749
|
+
</>
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
- **`slug`** is the page's slug as the orchestrator knows it — the same
|
|
755
|
+
leading-slash path `getPages()` returned, not the URL the visitor typed. Build
|
|
756
|
+
it with `buildSlug` rather than joining segments by hand.
|
|
757
|
+
- **`editorOrigin`** is the origin the overlay will `postMessage` to, and every
|
|
758
|
+
message is scoped to it. `resolveEditorContext` already returns it as its
|
|
759
|
+
third field; there is no need to plumb an env var through for this.
|
|
760
|
+
|
|
761
|
+
The component renders `null` outside an iframe, so it is inert on a normal page
|
|
762
|
+
load and safe to mount unconditionally within the preview route.
|
|
763
|
+
|
|
764
|
+
### Then turn selection on in the editor
|
|
765
|
+
|
|
766
|
+
Mounting the overlay is necessary and not sufficient. Clicking is gated on
|
|
767
|
+
`data-editor-selection-mode`, which the bridge sets only when the editor sends
|
|
768
|
+
`setSelectionMode {enabled: true}` — and the editor's own default is **off**.
|
|
769
|
+
|
|
770
|
+
So a correctly-wired integration's first browser session looks like this: the
|
|
771
|
+
site frames, renders, and does not respond to a click. Nothing is broken; the
|
|
772
|
+
picker is not on. Turn it on with the crosshair button in the chat composer
|
|
773
|
+
("Select element"), or press Esc to leave it again.
|
|
774
|
+
|
|
775
|
+
Check this before debugging anything else — an integrator who assumes their
|
|
776
|
+
markup is wrong can spend an afternoon proving that it isn't. In the iframe's
|
|
777
|
+
console:
|
|
778
|
+
|
|
779
|
+
```js
|
|
780
|
+
document.documentElement.hasAttribute("data-editor-active") // overlay mounted
|
|
781
|
+
document.documentElement.hasAttribute("data-editor-selection-mode") // picker on
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Both true and clicks still doing nothing is a real bug. The first true and the
|
|
785
|
+
second false is the default.
|
|
786
|
+
|
|
787
|
+
Once the picker is on, blocks are selectable. Individual *fields* are not,
|
|
788
|
+
until:
|
|
789
|
+
|
|
677
790
|
## Making fields editable
|
|
678
791
|
|
|
679
792
|
The manifest tells the editor which blocks exist and what props they take. It
|
package/dist/editor-overlay.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selection UI: block outlines, the type badge, the field pills, and the
|
|
3
|
+
* postMessage channel the editor talks to.
|
|
4
|
+
*
|
|
5
|
+
* Mount it inside the preview route. A preview page without it renders and
|
|
6
|
+
* frames correctly and simply does not respond to a click — which is a hard
|
|
7
|
+
* failure to diagnose, because nothing is broken, something is absent.
|
|
8
|
+
*
|
|
9
|
+
* @param slug The page's slug **as the orchestrator knows it**: the same
|
|
10
|
+
* leading-slash path `getPages()` returned, not the URL the visitor typed.
|
|
11
|
+
* `buildSlug(segments)` produces it from a catch-all route's params.
|
|
12
|
+
* @param editorOrigin The origin every message is scoped to. It is the third
|
|
13
|
+
* field of what `resolveEditorContext()` already returns — there is nothing
|
|
14
|
+
* to plumb through for it.
|
|
15
|
+
*
|
|
16
|
+
* Renders `null` outside an iframe, so it is inert on an ordinary page load.
|
|
17
|
+
*/
|
|
1
18
|
export declare function EditorOverlay({ slug, editorOrigin }: {
|
|
2
19
|
slug: string;
|
|
3
20
|
editorOrigin: string;
|
package/dist/editor-overlay.js
CHANGED
|
@@ -3,6 +3,23 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import dynamic from "next/dynamic";
|
|
4
4
|
import { useState, useEffect } from "react";
|
|
5
5
|
const PreviewBridgeLoader = dynamic(() => import("./editor-overlay-inner.js").then((m) => ({ default: m.EditorOverlayInner })), { ssr: false });
|
|
6
|
+
/**
|
|
7
|
+
* The selection UI: block outlines, the type badge, the field pills, and the
|
|
8
|
+
* postMessage channel the editor talks to.
|
|
9
|
+
*
|
|
10
|
+
* Mount it inside the preview route. A preview page without it renders and
|
|
11
|
+
* frames correctly and simply does not respond to a click — which is a hard
|
|
12
|
+
* failure to diagnose, because nothing is broken, something is absent.
|
|
13
|
+
*
|
|
14
|
+
* @param slug The page's slug **as the orchestrator knows it**: the same
|
|
15
|
+
* leading-slash path `getPages()` returned, not the URL the visitor typed.
|
|
16
|
+
* `buildSlug(segments)` produces it from a catch-all route's params.
|
|
17
|
+
* @param editorOrigin The origin every message is scoped to. It is the third
|
|
18
|
+
* field of what `resolveEditorContext()` already returns — there is nothing
|
|
19
|
+
* to plumb through for it.
|
|
20
|
+
*
|
|
21
|
+
* Renders `null` outside an iframe, so it is inert on an ordinary page load.
|
|
22
|
+
*/
|
|
6
23
|
export function EditorOverlay({ slug, editorOrigin }) {
|
|
7
24
|
const [inIframe, setInIframe] = useState(false);
|
|
8
25
|
useEffect(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/site-sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -107,16 +107,16 @@
|
|
|
107
107
|
],
|
|
108
108
|
"dependencies": {
|
|
109
109
|
"zod": "^4.3.6",
|
|
110
|
-
"@avocadostudio-ai/
|
|
111
|
-
"@avocadostudio-ai/
|
|
112
|
-
"@avocadostudio-ai/shared": "^0.3.
|
|
110
|
+
"@avocadostudio-ai/blocks": "^0.3.2",
|
|
111
|
+
"@avocadostudio-ai/preview-adapter": "^0.3.2",
|
|
112
|
+
"@avocadostudio-ai/shared": "^0.3.2"
|
|
113
113
|
},
|
|
114
114
|
"peerDependencies": {
|
|
115
115
|
"next": ">=15.0.0",
|
|
116
116
|
"react": ">=19.0.0",
|
|
117
117
|
"react-dom": ">=19.0.0",
|
|
118
118
|
"better-sqlite3": ">=12.0.0",
|
|
119
|
-
"@avocadostudio-ai/orchestrator-core": "^0.3.
|
|
119
|
+
"@avocadostudio-ai/orchestrator-core": "^0.3.2"
|
|
120
120
|
},
|
|
121
121
|
"peerDependenciesMeta": {
|
|
122
122
|
"@avocadostudio-ai/orchestrator-core": {
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"draft-mode"
|
|
145
145
|
],
|
|
146
146
|
"license": "Apache-2.0",
|
|
147
|
-
"homepage": "https://
|
|
147
|
+
"homepage": "https://docs.avocadostudio.dev",
|
|
148
148
|
"bugs": {
|
|
149
149
|
"url": "https://github.com/avocadostudio-ai/avocado/issues"
|
|
150
150
|
},
|