@craftware/crafty-sdk 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/attrs.d.ts +42 -0
- package/dist/attrs.d.ts.map +1 -0
- package/dist/attrs.js +60 -0
- package/dist/attrs.js.map +1 -0
- package/dist/cdn.d.ts +5 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +5 -0
- package/dist/cdn.js.map +1 -0
- package/dist/client.d.ts +12 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -8
- package/dist/client.js.map +1 -1
- package/dist/crafty-visual-editing.global.js +4 -0
- package/dist/crafty-visual-editing.global.js.map +7 -0
- package/dist/http.d.ts +7 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +32 -2
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/preview.d.ts +56 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +174 -0
- package/dist/preview.js.map +1 -0
- package/dist/resources/collections.d.ts +2 -2
- package/dist/resources/collections.d.ts.map +1 -1
- package/dist/resources/entries.d.ts +3 -2
- package/dist/resources/entries.d.ts.map +1 -1
- package/dist/resources/entries.js +20 -4
- package/dist/resources/entries.js.map +1 -1
- package/dist/resources/forms.d.ts +2 -2
- package/dist/resources/forms.d.ts.map +1 -1
- package/dist/resources/forms.js +1 -1
- package/dist/resources/forms.js.map +1 -1
- package/dist/resources/mediaCollections.d.ts +2 -2
- package/dist/resources/mediaCollections.d.ts.map +1 -1
- package/dist/resources/mediaFiles.d.ts +2 -2
- package/dist/resources/mediaFiles.d.ts.map +1 -1
- package/dist/resources/pages.d.ts +2 -2
- package/dist/resources/pages.d.ts.map +1 -1
- package/dist/resources/pages.js +4 -0
- package/dist/resources/pages.js.map +1 -1
- package/dist/types/public.d.ts +11 -2
- package/dist/types/public.d.ts.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/visual-editing/bridge.d.ts +35 -0
- package/dist/visual-editing/bridge.d.ts.map +1 -0
- package/dist/visual-editing/bridge.js +446 -0
- package/dist/visual-editing/bridge.js.map +1 -0
- package/dist/visual-editing/index.d.ts +10 -0
- package/dist/visual-editing/index.d.ts.map +1 -0
- package/dist/visual-editing/index.js +10 -0
- package/dist/visual-editing/index.js.map +1 -0
- package/dist/visual-editing/live-preview.d.ts +15 -0
- package/dist/visual-editing/live-preview.d.ts.map +1 -0
- package/dist/visual-editing/live-preview.js +63 -0
- package/dist/visual-editing/live-preview.js.map +1 -0
- package/dist/visual-editing/markers.d.ts +11 -0
- package/dist/visual-editing/markers.d.ts.map +1 -0
- package/dist/visual-editing/markers.js +44 -0
- package/dist/visual-editing/markers.js.map +1 -0
- package/dist/visual-editing/origin.d.ts +10 -0
- package/dist/visual-editing/origin.d.ts.map +1 -0
- package/dist/visual-editing/origin.js +37 -0
- package/dist/visual-editing/origin.js.map +1 -0
- package/dist/visual-editing/overlay.d.ts +13 -0
- package/dist/visual-editing/overlay.d.ts.map +1 -0
- package/dist/visual-editing/overlay.js +107 -0
- package/dist/visual-editing/overlay.js.map +1 -0
- package/dist/visual-editing/protocol.d.ts +83 -0
- package/dist/visual-editing/protocol.d.ts.map +1 -0
- package/dist/visual-editing/protocol.js +170 -0
- package/dist/visual-editing/protocol.js.map +1 -0
- package/package.json +13 -4
- package/skills/crafty-cms/SKILL.md +15 -1
- package/skills/crafty-cms/references/mcp-configuration.md +7 -0
- package/skills/crafty-cms/references/sdk-development.md +13 -1
- package/skills/crafty-cms/references/visual-editor.md +297 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: crafty-cms
|
|
3
|
-
description: "Build on Crafty CMS — configure sites, pages, blocks, collections, entries, forms, and media through the Crafty MCP server, then develop a frontend with the @craftware/crafty-sdk JavaScript SDK. Use when connecting an agent to a Crafty MCP server; calling Crafty MCP tools like list_sites, create_page, publish_form, or upload_media; installing or importing @craftware/crafty-sdk or createCraftyClient; fetching Crafty pages, entries, collections, or media; rendering page blocks; or
|
|
3
|
+
description: "Build on Crafty CMS — configure sites, pages, blocks, collections, entries, forms, and media through the Crafty MCP server, then develop a frontend with the @craftware/crafty-sdk JavaScript SDK. Use when connecting an agent to a Crafty MCP server; calling Crafty MCP tools like list_sites, create_page, publish_form, or upload_media; installing or importing @craftware/crafty-sdk or createCraftyClient; fetching Crafty pages, entries, collections, or media; rendering page blocks; submitting Crafty forms; or preparing a site for the Crafty visual editor (crafty.attrs, enableVisualEditing, preview tokens, configure_visual_editor)."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: craftware
|
|
@@ -64,6 +64,7 @@ Collections/entries: `list_collections` → `create_entry` (`collection_slug`, `
|
|
|
64
64
|
Forms: `create_form` (`site_id`, `name`, `slug`, `fields`) → `publish_form` → `list_form_submissions`.
|
|
65
65
|
Media: `upload_media` (`url`, `site_id`, optional `collection_name`).
|
|
66
66
|
Cache: `set_api_cache_status` (`site_id`, `enabled`) to toggle per-site; `flush_api_cache` (`site_id`, optional `resource`) to invalidate cached responses after bulk edits.
|
|
67
|
+
Visual editor: `configure_visual_editor` (`site_id`, optional `environment`, `frontend_url`, `routes`, `render_mode`, `upload_media_collection`) — see `references/visual-editor.md`.
|
|
67
68
|
|
|
68
69
|
> `update_page` **replaces all blocks** when `blocks` is supplied. `update_entry` creates a revision. `create_page` always starts as `draft`.
|
|
69
70
|
|
|
@@ -117,6 +118,17 @@ crafty deploy # uploads dist/ to the configured environment
|
|
|
117
118
|
|
|
118
119
|
> `crafty.json` is safe to commit. `.env` (which contains your API key) must be gitignored — use `.env.example` with placeholder values.
|
|
119
120
|
|
|
121
|
+
## Visual editor (click-to-edit) — always ask
|
|
122
|
+
|
|
123
|
+
Whenever you integrate Crafty into a frontend, ask the user exactly once:
|
|
124
|
+
|
|
125
|
+
> **¿Quieres dejar el sitio preparado para el editor visual de Crafty?**
|
|
126
|
+
|
|
127
|
+
- **Yes** → follow `references/visual-editor.md` completely: mark content with `crafty.attrs()` (buttons and links with `hrefField`; JSON paths with the stored index), deliver the preview token for the site's render mode, call `enableVisualEditing()` (pass `client` for `ssr` sites; React/Vue sites re-fetch and remount in `onRefresh`, hybrid sites cache the latest fetch), allow framing (`frame-ancestors`), check images are public, confirm which host really serves the domain before setting `frontend_url`, run `configure_visual_editor`, and verify (including hydration errors).
|
|
128
|
+
- **No** → add nothing and tell the user it can be enabled later by asking «habilita el editor visual de Crafty».
|
|
129
|
+
|
|
130
|
+
A later request like «habilita el editor visual de Crafty» on an existing site runs the Yes flow without asking again.
|
|
131
|
+
|
|
120
132
|
## Key distinction: MCP auth vs SDK API key
|
|
121
133
|
|
|
122
134
|
These are two different credentials — do not mix them:
|
|
@@ -133,8 +145,10 @@ These are two different credentials — do not mix them:
|
|
|
133
145
|
- **`update_entry` / `update_page` wipe fields you don't send** → both MCP tools do a **full replace**, not a merge. If you send only the new fields, all existing fields are deleted in that revision. Always fetch the current entry first (`get_entry`), merge your changes into the full field map, then call `update_entry` with the complete merged object.
|
|
134
146
|
- **Content not visible via the SDK** → it isn't published. Pages/forms must be published (`publish_page` / `publish_form`).
|
|
135
147
|
- **Confusing the two tokens** → the SDK `apiKey` (X-API-Key) is not an MCP Bearer token.
|
|
148
|
+
- **Stale skill copy** → a project copy of this skill (e.g. `.claude/skills/crafty-cms`) does not update with the SDK. After upgrading `@craftware/crafty-sdk`, copy `node_modules/@craftware/crafty-sdk/skills/crafty-cms` over it.
|
|
136
149
|
|
|
137
150
|
## References
|
|
138
151
|
|
|
139
152
|
- `references/mcp-configuration.md` — connecting every client, scopes, and the full MCP tool catalog with workflows.
|
|
140
153
|
- `references/sdk-development.md` — the complete SDK surface, block helpers, error handling, and examples.
|
|
154
|
+
- `references/visual-editor.md` — preparing a site for the visual editor: the mandatory question, marking, preview tokens, bridge, headers, `configure_visual_editor`, verification.
|
|
@@ -140,6 +140,7 @@ Form submissions may contain personal data, so they require their own scope.
|
|
|
140
140
|
| `upload_media` | `url`, `site_id` | `collection_name` (default `library`), `file_name` | downloads the file from the URL |
|
|
141
141
|
| `create_form` | `site_id`, `name`, `slug` | `description`, `fields` | `fields: [{ key, type, label, required? }]`; starts as `draft` |
|
|
142
142
|
| `update_form` | `form_id` | `name`, `slug`, `description`, `fields`, `notification_emails` | only supplied fields change; `fields`/`notification_emails` replace |
|
|
143
|
+
| `configure_visual_editor` | `site_id` | `environment` (default `production`), `frontend_url`, `routes` (`page_pattern`, `home_slug`, `collections`, `fixed`), `render_mode` (`ssr`/`hybrid`/`static`/`unknown`), `upload_media_collection` | visual editor settings of one environment; only sent keys change, `null` resets to the fallback; with only `site_id` it just reads; returns the effective settings |
|
|
143
144
|
|
|
144
145
|
### Publish (`crafty:publish`)
|
|
145
146
|
|
|
@@ -176,6 +177,12 @@ Form submissions may contain personal data, so they require their own scope.
|
|
|
176
177
|
1. `upload_media` with `url` and `site_id` (optionally `collection_name`).
|
|
177
178
|
2. `list_media` to confirm and retrieve URLs.
|
|
178
179
|
|
|
180
|
+
**Visual editor**
|
|
181
|
+
|
|
182
|
+
1. Prepare the frontend (see `visual-editor.md`).
|
|
183
|
+
2. `configure_visual_editor` with `site_id`, `frontend_url`, `routes`, and `render_mode`.
|
|
184
|
+
3. Ask the user to open **Editar sitio** in the admin to confirm click-to-edit works.
|
|
185
|
+
|
|
179
186
|
**API response cache management**
|
|
180
187
|
|
|
181
188
|
The public API has a tag-based response cache (enabled per environment). Per-site overrides take priority over the global setting.
|
|
@@ -23,7 +23,7 @@ const crafty = createCraftyClient({
|
|
|
23
23
|
});
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
`createCraftyClient(config)` returns a plain object with six resources
|
|
26
|
+
`createCraftyClient(config)` returns a plain object with six resources (`collections`, `entries`, `pages`, `forms`, `mediaCollections`, `mediaFiles`) and the visual editor helpers `attrs`, `attrsHtml` and `previewFromRequest`.
|
|
27
27
|
|
|
28
28
|
### Config options
|
|
29
29
|
|
|
@@ -35,6 +35,8 @@ const crafty = createCraftyClient({
|
|
|
35
35
|
| `fetch` | no | Custom fetch. Defaults to `globalThis.fetch`. If none is available, construction throws `CraftyError` (`NETWORK_ERROR`). |
|
|
36
36
|
| `headers` | no | Default headers merged into every request. |
|
|
37
37
|
| `timeoutMs` | no | Default per-request timeout. A timeout is only armed when the value is a number `> 0`. |
|
|
38
|
+
| `preview` | no | Visual editor preview token sent as `X-Crafty-Preview`. Omit in the browser (read from `?crafty_preview=`, kept in `sessionStorage`); pass `previewFromRequest(req)` on the server; `false` never sends one. |
|
|
39
|
+
| `onPreviewInvalid` | no | Called when a request that carried a token gets `X-Crafty-Preview-Status: invalid` (the response is the public content). |
|
|
38
40
|
|
|
39
41
|
## Per-call options
|
|
40
42
|
|
|
@@ -191,6 +193,16 @@ try {
|
|
|
191
193
|
}
|
|
192
194
|
```
|
|
193
195
|
|
|
196
|
+
## Visual editor helpers
|
|
197
|
+
|
|
198
|
+
- `attrs(target, field, { label? })` / `crafty.attrs(...)` → `{ "data-crafty-block" | "data-crafty-entry", "data-crafty-field", "data-crafty-label"? }` to spread on the element that renders the value; `{}` when the target has no valid id. `field` is a field key or a JSON path (`sections[3].title`).
|
|
199
|
+
- `attrsHtml(target, field, { label? })` → the same attributes as an escaped HTML string.
|
|
200
|
+
- `previewFromRequest(req)` → preview token of an incoming request (URL string, `URL`, Web `Request`, Node `req.url`, Express `req.query`) or `null`.
|
|
201
|
+
- `readPreviewToken()` → token in the browser, or `null`.
|
|
202
|
+
- `enableVisualEditing(options)` from `@craftware/crafty-sdk/visual-editing` → connects the page to the editor; no-op outside the admin iframe. Options: `onRefresh`, `adminOrigins`, `page`, `entries`, `touchHighlightMs`.
|
|
203
|
+
|
|
204
|
+
Full integration checklist: `visual-editor.md`.
|
|
205
|
+
|
|
194
206
|
## Notes
|
|
195
207
|
|
|
196
208
|
- `Accept: application/json` is set automatically unless overridden.
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# Preparing a site for the Crafty visual editor
|
|
2
|
+
|
|
3
|
+
The visual editor lets a non-technical user open their real site inside the Crafty admin, click (or tap) a text or image, edit it in a side panel, watch the change live, and publish. It only works when the frontend is prepared with `@craftware/crafty-sdk` **0.3.0 or later**; use **0.4.0 or later** so buttons and links select instead of navigating and `hrefField` is available. This guide is the complete checklist.
|
|
4
|
+
|
|
5
|
+
## The mandatory question
|
|
6
|
+
|
|
7
|
+
Whenever you integrate Crafty into a frontend (a new site, new pages or sections, or moving hardcoded content into Crafty), ask the user this exact question once, before you finish:
|
|
8
|
+
|
|
9
|
+
> **¿Quieres dejar el sitio preparado para el editor visual de Crafty?**
|
|
10
|
+
|
|
11
|
+
- **Yes** → run the whole "Yes flow" below.
|
|
12
|
+
- **No** → run the "No flow".
|
|
13
|
+
- If the user later asks "habilita el editor visual de Crafty" (or anything equivalent) for an existing site, do not ask again: run the Yes flow.
|
|
14
|
+
|
|
15
|
+
## No flow
|
|
16
|
+
|
|
17
|
+
Add nothing: no `attrs()`, no `enableVisualEditing()`, no preview token handling, no headers, no `configure_visual_editor` call. End your final message with this sentence for the user:
|
|
18
|
+
|
|
19
|
+
> El editor visual de Crafty no quedó activado. Cuando quieras activarlo, pide: «habilita el editor visual de Crafty».
|
|
20
|
+
|
|
21
|
+
## Yes flow
|
|
22
|
+
|
|
23
|
+
Do every step. Keep changes minimal and in the site's existing style.
|
|
24
|
+
|
|
25
|
+
### 1. Decide the render mode
|
|
26
|
+
|
|
27
|
+
| `render_mode` | How to recognise it | Token strategy (step 4) |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `ssr` | Content is fetched on the server for every request: Next.js dynamic routes / Server Components without static caching, Nuxt SSR, Astro `output: "server"`, Remix loaders, Express/Fastify templates. | Read it per request with `previewFromRequest()`. |
|
|
30
|
+
| `hybrid` | HTML is prerendered at build time **and** the browser fetches Crafty again on load (the Vite + prerender + client refresh pattern). | Automatic in the browser. |
|
|
31
|
+
| `static` | Content is baked at build time only; the browser never calls the Crafty API. | Add a browser re-fetch that only runs in preview. |
|
|
32
|
+
| `unknown` | You cannot tell. | Treat it like `hybrid` and tell the user. |
|
|
33
|
+
|
|
34
|
+
### 2. Upgrade the SDK
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @craftware/crafty-sdk@^0.4.0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The package ships this skill. If the project keeps its own copy (for example `.claude/skills/crafty-cms`), refresh it after every SDK upgrade so it matches the installed version:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
rm -rf .claude/skills/crafty-cms
|
|
44
|
+
cp -R node_modules/@craftware/crafty-sdk/skills/crafty-cms .claude/skills/crafty-cms
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 3. Mark every editable element
|
|
48
|
+
|
|
49
|
+
Use `attrs(target, field, { label })` (also available as `crafty.attrs` on the client). `target` is the block or entry the value came from; `field` is the field key or the JSON path of the value.
|
|
50
|
+
|
|
51
|
+
Rules:
|
|
52
|
+
|
|
53
|
+
- Mark only content that comes from Crafty. Never mark hardcoded text.
|
|
54
|
+
- Put the attributes on the innermost element whose **entire text** is the field value (`<h1>`, `<p>`, `<span>`, the `<a>` or `<button>` label, `<li>`). Live preview replaces that element's text, so an element that also contains icons or other fields must not be marked; wrap the value in a `<span>` and mark the span.
|
|
55
|
+
- Images from a media `reference` field: mark the `<img>` with the field key (`image`, `photo`).
|
|
56
|
+
- Buttons and links (CTAs): mark the label and pass the destination field as `hrefField`, so the editor shows the text and an «Enlace» input together and saves each as its own draft. A click anywhere on a link selects its marked label (the editor never follows links); on a real `<button>`, `<summary>` or `<label>` only a click on the marked element selects, so menus and accordions keep working. A link whose only editable value is its destination (a `url` field) is marked on the `<a>` itself; destinations update after saving the draft, not live while typing.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<Link to={cta.href} className="btn">
|
|
60
|
+
<span {...attrs(entry, `sections[${i}].cta.label`, { label: "Texto del botón", hrefField: `sections[${i}].cta.href`, hrefLabel: "Enlace del botón" })}>{cta.label}</span>
|
|
61
|
+
<Arrow />
|
|
62
|
+
</Link>
|
|
63
|
+
```
|
|
64
|
+
- Values inside `json` fields: use the exact path of the rendered value, e.g. `sections[2].title`, `hero.title_segments[0].text`, `extra.ctaPrimaryLabel`. Array indexes are positions in the **stored** JSON. When you render the array as stored, the loop index is that position (`` `sections[${i}].title` ``). When you filter, sort, split or reorder it (featured vs regular products, items sorted by `order`, only visible sections), keep the stored index with each item **before** transforming it and use that, never the rendered index:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Keep the stored position before filtering or sorting.
|
|
68
|
+
const items = section.items.map((item, index) => ({ item, index }));
|
|
69
|
+
const featured = items.filter(({ item }) => item.featured).sort((a, b) => a.item.order - b.item.order);
|
|
70
|
+
|
|
71
|
+
featured.map(({ item, index }) => (
|
|
72
|
+
<h3 key={index} {...attrs(entry, `sections[${sectionIndex}].items[${index}].title`, { label: "Título del producto" })}>{item.title}</h3>
|
|
73
|
+
));
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Section components that receive one slice of a JSON field can take a path-scoped helper instead of the whole path:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// editable(entry).at(`sections[${i}]`)("title", "Título") -> attrs(entry, `sections[${i}].title`, { label: "Título" })
|
|
80
|
+
export function editable(target, base = "") {
|
|
81
|
+
const join = (path) => (base ? `${base}.${path}` : path);
|
|
82
|
+
const edit = (field, label, options = {}) => attrs(target, join(field), { ...options, ...(label ? { label } : {}), ...(options.hrefField ? { hrefField: join(options.hrefField) } : {}) });
|
|
83
|
+
edit.at = (path) => editable(target, join(path));
|
|
84
|
+
return edit;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
- Editable types: `string`, `text`, `longText`, `url`, `number`, media `reference`, and string/number values inside `json`. Do not mark booleans, dates, selects, multi-references, or array items that are objects.
|
|
88
|
+
- `label` is a short Spanish name the client understands ("Título principal", "Texto del botón"). Without it the editor shows the field key.
|
|
89
|
+
- Pass blocks and entries as the API returns them. If an entry has fields named `order`, `data` and `type` at once, pass `{ kind: "entry", id: entry.id }` instead.
|
|
90
|
+
|
|
91
|
+
React / Next.js / Astro (JSX):
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { attrs } from "@craftware/crafty-sdk";
|
|
95
|
+
|
|
96
|
+
<h1 {...attrs(block, "title", { label: "Título principal" })}>{block.data.title}</h1>
|
|
97
|
+
<img {...attrs(block, "image", { label: "Imagen principal" })} src={imageUrl} alt="" />
|
|
98
|
+
|
|
99
|
+
{sections.map((section, i) => (
|
|
100
|
+
<section key={i}>
|
|
101
|
+
<h2 {...attrs(entry, `sections[${i}].title`, { label: "Título de la sección" })}>{section.title}</h2>
|
|
102
|
+
<p {...attrs(entry, `sections[${i}].body`, { label: "Texto de la sección" })}>{section.body}</p>
|
|
103
|
+
</section>
|
|
104
|
+
))}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Vue:
|
|
108
|
+
|
|
109
|
+
```vue
|
|
110
|
+
<h1 v-bind="attrs(entry, 'hero.title', { label: 'Título principal' })">{{ entry.hero.title }}</h1>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
HTML string templates (vanilla TS):
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { attrsHtml } from "@craftware/crafty-sdk";
|
|
117
|
+
|
|
118
|
+
html += `<h1 ${attrsHtml(entry, "extra.title", { label: "Título" })}>${escapeHtml(extra.title)}</h1>`;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4. Deliver the preview token
|
|
122
|
+
|
|
123
|
+
The editor loads the site with `?crafty_preview=<token>`. Requests that carry it as `X-Crafty-Preview` get unpublished drafts and bypass the API cache.
|
|
124
|
+
|
|
125
|
+
- **hybrid / SPA:** nothing to do. A client created in the browser reads the token from the URL, keeps it in `sessionStorage`, and sends it on every request. Make sure the browser re-fetch really runs on every page load.
|
|
126
|
+
- **ssr:** create the client **per request** with the token, never share a module-level client that received one, and never cache a preview response:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { createCraftyClient, previewFromRequest } from "@craftware/crafty-sdk";
|
|
130
|
+
|
|
131
|
+
// Express / Node: previewFromRequest(req) Web Request: previewFromRequest(request)
|
|
132
|
+
// Next.js App Router: previewFromRequest({ query: await searchParams })
|
|
133
|
+
const preview = previewFromRequest(req);
|
|
134
|
+
const crafty = createCraftyClient({ baseUrl: process.env.CRAFTY_URL!, apiKey: process.env.CRAFTY_API_KEY!, preview });
|
|
135
|
+
|
|
136
|
+
if (preview) {
|
|
137
|
+
res.setHeader("Cache-Control", "no-store"); // Next.js: call noStore() / use dynamic rendering for this request
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Server-rendered pages should also tell the editor what they show (step 5, `page` / `entries`).
|
|
142
|
+
- **static:** the built HTML has no drafts, so re-fetch in the browser only when previewing and re-render:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { readPreviewToken } from "@craftware/crafty-sdk";
|
|
146
|
+
|
|
147
|
+
if (readPreviewToken()) {
|
|
148
|
+
const fresh = await crafty.entries.list("site-pages");
|
|
149
|
+
renderPage(fresh.data); // the same function the build uses
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
If the site cannot re-render in the browser, set `render_mode` to `static` anyway and tell the user that the editor will show changes only after saving and reloading, and the public site only after a deploy.
|
|
154
|
+
|
|
155
|
+
### 5. Turn on the bridge
|
|
156
|
+
|
|
157
|
+
Call it once in the browser entry (it keeps watching the DOM, so it can run before or after the first render):
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { enableVisualEditing } from "@craftware/crafty-sdk/visual-editing";
|
|
161
|
+
|
|
162
|
+
enableVisualEditing({
|
|
163
|
+
onRefresh: () => reloadCraftyContent(), // re-fetch + re-render; omit it to let the editor reload the page
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- It does nothing for normal visitors (it needs the admin iframe, a token, and an allowed admin origin), so ship it to production.
|
|
168
|
+
- Even then it stays inert until the backend confirms the token — a response with `X-Crafty-Preview-Status: active`, normally the site's own first Crafty request for that page. **`ssr` sites where the browser never calls the Crafty API itself must pass `client`** (a `CraftyClient`, typically the same one already created for that render mode) so the bridge can confirm the token itself with one lightweight request (`pages.list({ per_page: 1 })`): `enableVisualEditing({ client: crafty })`. `hybrid` and `static` sites already make a browser-side request that carries the header, so `client` is optional for them. If the API answers `invalid`, the bridge stays off and tells the admin instead of activating.
|
|
169
|
+
- If the site fetches Crafty through something other than this SDK's client (so neither path above ever runs), report `X-Crafty-Preview-Status` yourself with `notifyPreviewActive()` / `notifyPreviewInvalid()` from `@craftware/crafty-sdk/visual-editing` (also exports `getPreviewStatus()` and the `PREVIEW_ACTIVE_EVENT` / `PREVIEW_INVALID_EVENT` event names).
|
|
170
|
+
- `onRefresh`: register it when the site can re-fetch Crafty and re-render without a reload (SPAs, hybrid sites). Otherwise omit it.
|
|
171
|
+
- **React, Vue and other frameworks:** live preview writes text straight into the DOM, into nodes the framework owns. Reconciling against those nodes after a refresh can leave stale text or crash (for example when the element also renders `<strong>` from an emphasis helper). In `onRefresh`, re-fetch and then **remount** the tree (change a `key` on the root) instead of patching it:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
// main.tsx (React)
|
|
175
|
+
let remount = () => {};
|
|
176
|
+
function Root() {
|
|
177
|
+
const [version, setVersion] = useState(0);
|
|
178
|
+
remount = () => setVersion((v) => v + 1);
|
|
179
|
+
return <App key={version} />;
|
|
180
|
+
}
|
|
181
|
+
enableVisualEditing({
|
|
182
|
+
onRefresh: async () => {
|
|
183
|
+
await refreshCrafty(); // re-fetch and update the content cache below
|
|
184
|
+
remount();
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Vue: the same with `<App :key="version" />` on the root component.
|
|
190
|
+
- **Hybrid sites: cache the latest fetch.** A remount (or a client-side navigation) must render the content fetched last (with drafts), not the build-time data embedded in the HTML. Keep the latest response per collection or page in a module-level cache, read it first when a component mounts, and update it in the refresh:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const cache = new Map<string, unknown>();
|
|
194
|
+
export async function loadCollection(slug: string) {
|
|
195
|
+
const fresh = await crafty.entries.list(slug);
|
|
196
|
+
cache.set(slug, fresh.data);
|
|
197
|
+
return fresh.data;
|
|
198
|
+
}
|
|
199
|
+
export const cachedCollection = (slug: string, buildTime: unknown) => cache.get(slug) ?? buildTime;
|
|
200
|
+
export const refreshCrafty = () => Promise.all([...cache.keys()].map(loadCollection));
|
|
201
|
+
```
|
|
202
|
+
- SSR: pass what the server rendered, e.g. `enableVisualEditing({ client: crafty, page: { id: page.id, slug: page.slug }, entries: [{ id: post.id, collection: "blog" }] })`.
|
|
203
|
+
- `adminOrigins`: only when the tenant's admin is not on `https://<tenant>.crafty.do` (custom admin domain, or local development such as `http://<tenant>.crafty.test`). It replaces the default list.
|
|
204
|
+
- No bundler:
|
|
205
|
+
|
|
206
|
+
```html
|
|
207
|
+
<script src="https://cdn.jsdelivr.net/npm/@craftware/crafty-sdk@0.4/dist/crafty-visual-editing.global.js"></script>
|
|
208
|
+
<script>CraftyVisualEditing.enableVisualEditing();</script>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Pin an exact version (`@0.4.0`) with an `integrity="sha384-…" crossorigin="anonymous"` attribute (jsDelivr shows the hash) when the site uses Subresource Integrity.
|
|
212
|
+
|
|
213
|
+
### 6. Allow the admin to frame the site
|
|
214
|
+
|
|
215
|
+
The site must accept being framed by the admin. Sites deployed to Crafty Hosting get this header from Crafty; for any other host add it (and remove any `X-Frame-Options: DENY` / `SAMEORIGIN`):
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
Content-Security-Policy: frame-ancestors 'self' https://<tenant>.crafty.do
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- Netlify / Cloudflare Pages (`_headers` in the build output):
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
/*
|
|
225
|
+
Content-Security-Policy: frame-ancestors 'self' https://<tenant>.crafty.do
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- Vercel (`vercel.json`):
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{ "headers": [{ "source": "/(.*)", "headers": [{ "key": "Content-Security-Policy", "value": "frame-ancestors 'self' https://<tenant>.crafty.do" }] }] }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
- Nginx: `add_header Content-Security-Policy "frame-ancestors 'self' https://<tenant>.crafty.do" always;`
|
|
235
|
+
|
|
236
|
+
If the site already sends a `Content-Security-Policy`, add the `frame-ancestors` directive to it instead of sending a second header.
|
|
237
|
+
|
|
238
|
+
### 7. Make sure images come from public repositories
|
|
239
|
+
|
|
240
|
+
Static and hybrid sites keep image URLs in their HTML; private repositories give expiring URLs. Check with `list_media` (each file has `is_public`) or `crafty.mediaCollections.list()`. If images the site shows live in a private repository, tell the user: «Las imágenes del repositorio X son privadas y sus enlaces caducan en sitios estáticos. Márcalo como público en Crafty → Media.» Uploads made from the visual editor go to the public "Editor visual" repository by default.
|
|
241
|
+
|
|
242
|
+
### 8. Configure Crafty
|
|
243
|
+
|
|
244
|
+
Call `configure_visual_editor` (scope `crafty:write`) with what you learned:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"site_id": 3,
|
|
249
|
+
"environment": "production",
|
|
250
|
+
"frontend_url": "https://www.example.com",
|
|
251
|
+
"routes": {
|
|
252
|
+
"page_pattern": "/{slug}",
|
|
253
|
+
"home_slug": "home",
|
|
254
|
+
"collections": { "blog": "/blog/{slug}" },
|
|
255
|
+
"fixed": ["/", "/en", "/blog"]
|
|
256
|
+
},
|
|
257
|
+
"render_mode": "hybrid"
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
- `frontend_url`: the public URL of that environment where **this** build runs (omit it when it is the site's domain in production or the Crafty Hosting URL). Before setting a domain, check which host actually serves it: a domain can point at a different app than the one you prepared (another server, an older site), and then the editor frames a page without the SDK.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
curl -sI https://www.example.com/ | grep -iE "^(server|x-frame-options|content-security-policy|set-cookie|location)"
|
|
265
|
+
curl -sI https://<site>.pages.crafty.do/ | grep -iE "^(server|x-frame-options|content-security-policy)"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Crafty Hosting answers with `content-security-policy: frame-ancestors 'self' https://<tenant>.crafty.do`. `x-frame-options: deny`, a foreign framework cookie (for example `laravel_session`) or a different `server` means another app serves that domain: do not use it as `frontend_url`; use the Crafty Hosting URL (omit `frontend_url`) or the URL that really serves this build.
|
|
269
|
+
- `routes.page_pattern` / `home_slug`: only if the site renders Crafty **pages**; patterns start with `/` and contain `{slug}`.
|
|
270
|
+
- `routes.collections`: collections whose entries have their own URL; patterns contain `{slug}` or `{id}`.
|
|
271
|
+
- `routes.fixed`: every other URL that shows Crafty content (home in other languages, listing pages, single-page sites: just `["/"]`).
|
|
272
|
+
- `render_mode`: from step 1.
|
|
273
|
+
- Repeat with `environment: "development"` (or others) when they have their own URL.
|
|
274
|
+
- Calling it with only `site_id` returns the current settings without changing anything.
|
|
275
|
+
|
|
276
|
+
### 9. Verify
|
|
277
|
+
|
|
278
|
+
1. Build and type-check the site.
|
|
279
|
+
2. Run it locally and open a page with `?crafty_preview=test`. In DevTools:
|
|
280
|
+
- `document.querySelectorAll("[data-crafty-field]").length` is greater than 0 on every page type (home, a page, a collection entry).
|
|
281
|
+
- Requests to the Crafty API carry `X-Crafty-Preview: test`, and the responses say `X-Crafty-Preview-Status: invalid` (expected: `test` is not a real token; the content stays public).
|
|
282
|
+
- Nothing is highlighted: outside the admin iframe the bridge stays off.
|
|
283
|
+
- The console has no hydration errors (React #418/#423, Vue "Hydration mismatch"). Compare with a build without your changes: if they were already there, say so to the user instead of hiding them; if they are new, the marked markup differs between the prerender and the client render.
|
|
284
|
+
- Prerendered (hybrid/static) pages first show the published content and then the drafts once the browser re-fetch finishes. A short flash of published text in preview is expected; if drafts never appear, the preview re-fetch is missing or a remount reads build-time data (see the cache in step 5).
|
|
285
|
+
3. After deploying, ask the user to open Crafty → the site → **Editar sitio** (`https://<tenant>.crafty.do/sites/<site_id>/visual-editor?path=/`). If hovering highlights texts and a click opens the field, the editor received `crafty:ready`. If it shows «Este sitio aún no tiene edición con clic activada», go through the troubleshooting list.
|
|
286
|
+
|
|
287
|
+
End with a short summary for the user in Spanish: what was marked, the render mode, and what they must do after publishing (nothing for `ssr`/`hybrid`; a deploy for `static`).
|
|
288
|
+
|
|
289
|
+
## Troubleshooting
|
|
290
|
+
|
|
291
|
+
- **The editor shows «Este sitio aún no tiene edición con clic activada»** — the iframe is blocked (browser console: `frame-ancestors` or `X-Frame-Options`), `enableVisualEditing()` is not in the bundle of that page, the SDK is older than 0.3.0, the admin origin is not allowed (custom admin domain → `adminOrigins`), a redirect dropped the query string (trailing-slash or locale redirects must keep `?crafty_preview=`), or the site is `ssr` and never confirmed the token because `client` was not passed to `enableVisualEditing()`.
|
|
292
|
+
- **«Esta URL no mostró la página …»** — `routes` do not match the site's URLs; fix them with `configure_visual_editor`.
|
|
293
|
+
- **Saved drafts do not appear in the editor** — SSR: the client was created once at module level or the response was cached; static: the preview re-fetch is missing.
|
|
294
|
+
- **Text reverts or the page crashes after saving** — a framework reconciled DOM that live preview had changed; remount in `onRefresh` (step 5).
|
|
295
|
+
- **Typing does not update the text live** — the attributes are on a wrapper that contains more than the value, or the field is rich text (`longText` shows after saving the draft).
|
|
296
|
+
- **Clicking a menu link opens its text instead of the page** — expected: with the bridge active, marked menu labels are selected for editing, not followed. Change pages with the editor's route selector (unmarked internal links still navigate).
|
|
297
|
+
- **Clicking an editable card navigates away** — the SDK is older than 0.4.0 (earlier versions only selected when the click landed exactly on the marked element) or nothing inside the link is marked; mark its label.
|