@cloudcannon/editable-regions 0.0.20-rc.3 → 0.0.20
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 +885 -0
- package/helpers/cloudcannon.mjs +20 -9
- package/helpers/hydrate-editable-regions.ts +36 -3
- package/integrations/hugo/browser/errors.ts +8 -2
- package/integrations/react.mjs +63 -1
- package/integrations/svelte/EditableRegions.svelte +80 -0
- package/integrations/{svelte.mjs → svelte/index.mjs} +3 -1
- package/integrations/vue.mjs +97 -1
- package/nodes/editable-array-item.ts +5 -1
- package/nodes/editable-component.ts +10 -4
- package/nodes/editable-image.ts +80 -75
- package/nodes/editable.ts +28 -5
- package/package.json +17 -16
- package/types/react.d.ts +20 -6
- package/types/svelte.d.ts +10 -0
- package/types/vue.d.ts +14 -1
- package/integrations/liquid/README.md +0 -677
package/README.md
CHANGED
|
@@ -3,3 +3,888 @@
|
|
|
3
3
|
Visual Editing for the [CloudCannon](https://cloudcannon.com/) CMS.
|
|
4
4
|
|
|
5
5
|
[<img src="https://img.shields.io/npm/v/@cloudcannon%2Feditable-regions?logo=npm" alt="version badge">](https://www.npmjs.com/package/@cloudcannon%2Feditable-regions)
|
|
6
|
+
|
|
7
|
+
Editable Regions let you mark up any HTML output with attributes (or web components) that tell CloudCannon's Visual Editor exactly what is editable: text, images, arrays, components, and hard-coded source markup. Edits re-render the preview live and write your changes back to your structured data or source files.
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Quickstart](#quickstart)
|
|
13
|
+
- [Astro](#astro)
|
|
14
|
+
- [Eleventy](#eleventy)
|
|
15
|
+
- [Hugo](#hugo)
|
|
16
|
+
- [React](#react)
|
|
17
|
+
- [Svelte](#svelte)
|
|
18
|
+
- [Vue](#vue)
|
|
19
|
+
- [Next.js](#nextjs)
|
|
20
|
+
- [SvelteKit](#sveltekit)
|
|
21
|
+
- [Nuxt](#nuxt)
|
|
22
|
+
- [How editable regions work](#how-editable-regions-work)
|
|
23
|
+
- [Defining editable regions](#defining-editable-regions)
|
|
24
|
+
- [Passing values to editable regions](#passing-values-to-editable-regions)
|
|
25
|
+
- [Value regions](#value-regions)
|
|
26
|
+
- [Text](#text)
|
|
27
|
+
- [Image](#image)
|
|
28
|
+
- [Structural regions](#structural-regions)
|
|
29
|
+
- [Components](#components)
|
|
30
|
+
- [Arrays and array items](#arrays-and-array-items)
|
|
31
|
+
- [How prop selectors compose](#how-prop-selectors-compose)
|
|
32
|
+
- [Page building](#page-building)
|
|
33
|
+
- [Referencing external data](#referencing-external-data)
|
|
34
|
+
- [Literal props](#literal-props)
|
|
35
|
+
- [Source regions](#source-regions)
|
|
36
|
+
- [How they work](#how-they-work)
|
|
37
|
+
- [Limitations](#limitations)
|
|
38
|
+
- [When to use them](#when-to-use-them)
|
|
39
|
+
- [Options](#options-4)
|
|
40
|
+
|
|
41
|
+
For integration-specific setup and options checkout the dedicated docs pages:
|
|
42
|
+
|
|
43
|
+
- [Astro](docs/astro.md)
|
|
44
|
+
- [Eleventy](docs/eleventy.md).
|
|
45
|
+
- [Hugo](docs/hugo.md).
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
Install the package from npm:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npm install @cloudcannon/editable-regions
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For Hugo sites, install the module at a pinned version:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
hugo mod get github.com/CloudCannon/editable-regions@<version>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quickstart
|
|
62
|
+
|
|
63
|
+
### Astro
|
|
64
|
+
|
|
65
|
+
Add the integration to `astro.config.mjs`:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { defineConfig } from "astro/config";
|
|
69
|
+
import editableRegions from "@cloudcannon/editable-regions/astro-integration";
|
|
70
|
+
|
|
71
|
+
export default defineConfig({
|
|
72
|
+
integrations: [editableRegions()],
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Create a registration script that imports and registers each component that you want to live edit:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
// src/scripts/register-components.js
|
|
80
|
+
import { registerAstroComponent } from "@cloudcannon/editable-regions/astro";
|
|
81
|
+
import CTA from "../components/CTA.astro";
|
|
82
|
+
|
|
83
|
+
registerAstroComponent("cta", CTA);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Load that script from your layout:
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<script>
|
|
90
|
+
// Check that the page is loaded in CloudCannon before loading your Editable Components
|
|
91
|
+
if (window.inEditorMode) {
|
|
92
|
+
import("../scripts/register-components.js").catch((error) => {
|
|
93
|
+
console.warn("Failed to load CloudCannon component registration:", error);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If your Astro components contain React, Svelte, or Vue components, add the matching side-effect renderer import to the same registration file:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import "@cloudcannon/editable-regions/astro-react-renderer";
|
|
103
|
+
import "@cloudcannon/editable-regions/astro-svelte-renderer";
|
|
104
|
+
import "@cloudcannon/editable-regions/astro-vue-renderer";
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Then mark up your templates:
|
|
108
|
+
|
|
109
|
+
```astro
|
|
110
|
+
<h1 data-editable="text" data-prop="title">{title}</h1>
|
|
111
|
+
<editable-component data-component="cta" data-prop="cta">
|
|
112
|
+
<CTA {...cta} />
|
|
113
|
+
</editable-component>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Eleventy
|
|
117
|
+
|
|
118
|
+
Add the plugin to `eleventy.config.mjs`:
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
import editableRegions from "@cloudcannon/editable-regions/eleventy";
|
|
122
|
+
|
|
123
|
+
export default function (eleventyConfig) {
|
|
124
|
+
eleventyConfig.addPlugin(editableRegions);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
dir: { input: "src", includes: "_includes", output: "_site" },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
On a CommonJS config, use require instead:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
const editableRegions = require("@cloudcannon/editable-regions/eleventy");
|
|
136
|
+
|
|
137
|
+
module.exports = function (eleventyConfig) {
|
|
138
|
+
eleventyConfig.addPlugin(editableRegions);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
dir: { input: "src", includes: "_includes", output: "_site" },
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
After every build, the plugin emits `register-components.js` into your output directory, a bundle containing a client-side Liquid engine, your filters/shortcodes/tags (auto-mirrored from your real Eleventy config), and your include templates. Load it in your layout:
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<script>
|
|
150
|
+
// Check that the page is loaded in CloudCannon before loading your Editable Components
|
|
151
|
+
if (window.inEditorMode) {
|
|
152
|
+
import("/register-components.js").catch((error) => {
|
|
153
|
+
console.warn("Failed to load CloudCannon component registration:", error);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
</script>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The plugin automatically registers your Liquid includes as components.
|
|
160
|
+
|
|
161
|
+
```liquid
|
|
162
|
+
<div data-editable="component" data-component="author-card" data-prop-author-name="authorName">
|
|
163
|
+
{% include "author-card", author-name: authorName %}
|
|
164
|
+
</div>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The plugin also registers an `includeWith` tag for spreading a props object into an include:
|
|
168
|
+
|
|
169
|
+
```liquid
|
|
170
|
+
<div data-editable="component" data-component="post-meta" data-prop="postMeta">
|
|
171
|
+
{% includeWith "post-meta", postMeta %}
|
|
172
|
+
</div>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Hugo
|
|
176
|
+
|
|
177
|
+
Install the package as a Hugo module:
|
|
178
|
+
|
|
179
|
+
```toml
|
|
180
|
+
# hugo.toml
|
|
181
|
+
[module]
|
|
182
|
+
[[module.imports]]
|
|
183
|
+
path = "github.com/CloudCannon/editable-regions"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Include the partial in your site `<head>`:
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
{{ partial "editable-regions" . }}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The module automatically registers your Hugo partials as components.
|
|
193
|
+
|
|
194
|
+
```html
|
|
195
|
+
<div data-editable="component" data-component="card" data-prop="card">
|
|
196
|
+
{{- partial "card" .Params.card -}}
|
|
197
|
+
</div>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### React
|
|
201
|
+
|
|
202
|
+
For a standalone React SPA, register your components from your client entry point:
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
// src/index.js
|
|
206
|
+
import { registerReactComponent } from "@cloudcannon/editable-regions/react";
|
|
207
|
+
import CTA from "./components/CTA.jsx";
|
|
208
|
+
|
|
209
|
+
registerReactComponent("cta", CTA);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Then mark up your templates:
|
|
213
|
+
|
|
214
|
+
```jsx
|
|
215
|
+
<editable-component data-component="cta" data-prop="cta">
|
|
216
|
+
<CTA {...props.cta} />
|
|
217
|
+
</editable-component>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Svelte
|
|
221
|
+
|
|
222
|
+
For a standalone Svelte SPA, register your components from your client entry point:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
// src/index.js
|
|
226
|
+
import { registerSvelteComponent } from "@cloudcannon/editable-regions/svelte";
|
|
227
|
+
import CTA from "./components/CTA.svelte";
|
|
228
|
+
|
|
229
|
+
registerSvelteComponent("cta", CTA);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Then mark up your templates:
|
|
233
|
+
|
|
234
|
+
```svelte
|
|
235
|
+
<editable-component data-component="cta" data-prop="cta">
|
|
236
|
+
<CTA {...cta} />
|
|
237
|
+
</editable-component>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Vue
|
|
241
|
+
|
|
242
|
+
For a standalone Vue SPA, register your components from your client entry point:
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
// src/index.js
|
|
246
|
+
import { registerVueComponent } from "@cloudcannon/editable-regions/vue";
|
|
247
|
+
import CTA from "./components/CTA.vue";
|
|
248
|
+
|
|
249
|
+
registerVueComponent("cta", CTA);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Then mark up your templates:
|
|
253
|
+
|
|
254
|
+
```vue
|
|
255
|
+
<editable-component data-component="cta" data-prop="cta">
|
|
256
|
+
<CTA v-bind="cta" />
|
|
257
|
+
</editable-component>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Next.js
|
|
261
|
+
|
|
262
|
+
In a Next.js app, wrap your page content in the `EditableRegions` root component. Editable
|
|
263
|
+
regions inside it connect only after the app has finished hydrating:
|
|
264
|
+
|
|
265
|
+
```jsx
|
|
266
|
+
// app/layout.js
|
|
267
|
+
import { EditableRegions } from "@cloudcannon/editable-regions/react";
|
|
268
|
+
|
|
269
|
+
export default function RootLayout({ children }) {
|
|
270
|
+
return (
|
|
271
|
+
<html lang="en">
|
|
272
|
+
<body>
|
|
273
|
+
<EditableRegions tag="main">{children}</EditableRegions>
|
|
274
|
+
</body>
|
|
275
|
+
</html>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Then mark up your templates — pages stay server components, the region markup is plain HTML:
|
|
281
|
+
|
|
282
|
+
```jsx
|
|
283
|
+
// app/page.js
|
|
284
|
+
export default function Page() {
|
|
285
|
+
return (
|
|
286
|
+
<h1>
|
|
287
|
+
<editable-text data-prop="title">Hello from Next.js</editable-text>
|
|
288
|
+
</h1>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Register your components from a client component, gated on `window.inEditorMode` so the code
|
|
294
|
+
stays out of production bundles:
|
|
295
|
+
|
|
296
|
+
```jsx
|
|
297
|
+
// app/components/CloudCannon.js
|
|
298
|
+
"use client";
|
|
299
|
+
|
|
300
|
+
import { useEffect } from "react";
|
|
301
|
+
import { registerReactComponent } from "@cloudcannon/editable-regions/react";
|
|
302
|
+
import CTA from "./CTA.jsx";
|
|
303
|
+
|
|
304
|
+
export default function CloudCannon() {
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (window.inEditorMode) {
|
|
307
|
+
registerReactComponent("cta", CTA);
|
|
308
|
+
}
|
|
309
|
+
}, []);
|
|
310
|
+
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
For a fully static site, export the pages as plain HTML — the build writes the static site to
|
|
316
|
+
`out/` with no server runtime needed:
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
// next.config.js
|
|
320
|
+
const nextConfig = {
|
|
321
|
+
output: "export",
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export default nextConfig;
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### SvelteKit
|
|
328
|
+
|
|
329
|
+
In a SvelteKit app, wrap the routed page in the `EditableRegions` root component in the root
|
|
330
|
+
layout. Editable regions inside it connect only after the app has finished hydrating:
|
|
331
|
+
|
|
332
|
+
```svelte
|
|
333
|
+
<!-- src/routes/+layout.svelte -->
|
|
334
|
+
<script>
|
|
335
|
+
import { onMount } from "svelte";
|
|
336
|
+
import {
|
|
337
|
+
EditableRegions,
|
|
338
|
+
registerSvelteComponent,
|
|
339
|
+
} from "@cloudcannon/editable-regions/svelte";
|
|
340
|
+
import CTA from "$lib/components/CTA.svelte";
|
|
341
|
+
|
|
342
|
+
let { children } = $props();
|
|
343
|
+
|
|
344
|
+
onMount(() => {
|
|
345
|
+
if (window.inEditorMode) {
|
|
346
|
+
registerSvelteComponent("cta", CTA);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
</script>
|
|
350
|
+
|
|
351
|
+
<EditableRegions tag="main">
|
|
352
|
+
{@render children()}
|
|
353
|
+
</EditableRegions>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Then mark up your templates:
|
|
357
|
+
|
|
358
|
+
```svelte
|
|
359
|
+
<!-- src/routes/+page.svelte -->
|
|
360
|
+
<h1>
|
|
361
|
+
<editable-text data-prop="title">Hello from SvelteKit</editable-text>
|
|
362
|
+
</h1>
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
For a fully static site, use the static adapter and prerender every route — the build writes the
|
|
366
|
+
static site to `build/`:
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
// svelte.config.js
|
|
370
|
+
import adapter from "@sveltejs/adapter-static";
|
|
371
|
+
|
|
372
|
+
const config = {
|
|
373
|
+
kit: {
|
|
374
|
+
adapter: adapter(),
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export default config;
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
// src/routes/+layout.js
|
|
383
|
+
export const prerender = true;
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Nuxt
|
|
387
|
+
|
|
388
|
+
In a Nuxt app, wrap the routed page in the `EditableRegions` root component in the layout.
|
|
389
|
+
Editable regions inside it connect only after the app has finished hydrating:
|
|
390
|
+
|
|
391
|
+
```vue
|
|
392
|
+
<!-- app/layouts/default.vue -->
|
|
393
|
+
<script setup>
|
|
394
|
+
import { EditableRegions } from "@cloudcannon/editable-regions/vue";
|
|
395
|
+
</script>
|
|
396
|
+
|
|
397
|
+
<template>
|
|
398
|
+
<EditableRegions tag="main">
|
|
399
|
+
<slot />
|
|
400
|
+
</EditableRegions>
|
|
401
|
+
</template>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Tell Vue to treat the `editable-*` elements as native elements rather than components:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
// nuxt.config.ts
|
|
408
|
+
export default defineNuxtConfig({
|
|
409
|
+
vue: {
|
|
410
|
+
compilerOptions: {
|
|
411
|
+
isCustomElement: (tag) => tag.startsWith("editable-"),
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Then mark up your templates:
|
|
418
|
+
|
|
419
|
+
```vue
|
|
420
|
+
<!-- app/pages/index.vue -->
|
|
421
|
+
<template>
|
|
422
|
+
<h1>
|
|
423
|
+
<editable-text data-prop="title">Hello from Nuxt</editable-text>
|
|
424
|
+
</h1>
|
|
425
|
+
</template>
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Register your components from a client plugin, gated on `window.inEditorMode` so the code stays
|
|
429
|
+
out of production bundles:
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
// app/plugins/cloudcannon.client.ts
|
|
433
|
+
import { registerVueComponent } from "@cloudcannon/editable-regions/vue";
|
|
434
|
+
import CTA from "../components/CTA.vue";
|
|
435
|
+
|
|
436
|
+
export default defineNuxtPlugin(() => {
|
|
437
|
+
if (window.inEditorMode) {
|
|
438
|
+
registerVueComponent("cta", CTA);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
For a fully static site, configure Nitro to prerender the site by crawling from the index page,
|
|
444
|
+
then build with `nuxt generate` — the build writes the static site to `.output/public`:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
// nuxt.config.ts
|
|
448
|
+
export default defineNuxtConfig({
|
|
449
|
+
nitro: {
|
|
450
|
+
prerender: {
|
|
451
|
+
crawlLinks: true,
|
|
452
|
+
routes: ["/"],
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## How editable regions work
|
|
459
|
+
|
|
460
|
+
An editable region is a piece of your page that the Visual Editor can edit in place. When your page is loaded in the Visual Editor, CloudCannon hydrates your elements into editable regions. Outside of CloudCannon your elements are plain HTML; your users see exactly the markup you authored.
|
|
461
|
+
|
|
462
|
+
The Editable Regions can edit any structured data configured in your CloudCannon site, including your page front matter and content, your datasets, and your collections.
|
|
463
|
+
|
|
464
|
+
### Defining editable regions
|
|
465
|
+
|
|
466
|
+
You define one by adding an HTML attribute to an existing element, or by wrapping content in the equivalent web component:
|
|
467
|
+
|
|
468
|
+
| Attribute | Web component |
|
|
469
|
+
| --- | --- |
|
|
470
|
+
| `data-editable="text"` | `<editable-text>` |
|
|
471
|
+
| `data-editable="image"` | `<editable-image>` |
|
|
472
|
+
| `data-editable="source"` | `<editable-source>` |
|
|
473
|
+
| `data-editable="array"` | `<editable-array>` |
|
|
474
|
+
| `data-editable="array-item"` | `<editable-array-item>` |
|
|
475
|
+
| `data-editable="component"` | `<editable-component>` |
|
|
476
|
+
|
|
477
|
+
The two forms are equivalent. Data attributes are useful when you are attaching editable regions to existing markup, whereas the web components are useful when there isn't existing markup to attach an attribute to or you don't have direct access to the markup.
|
|
478
|
+
|
|
479
|
+
```astro
|
|
480
|
+
<h1 data-editable="text" data-prop="title">{title}</h1>
|
|
481
|
+
|
|
482
|
+
<editable-image data-prop-src="src">
|
|
483
|
+
<MyFigureComponent src={src} />
|
|
484
|
+
</editable-image>
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Passing values to editable regions
|
|
488
|
+
|
|
489
|
+
Every editable region receives the value it edits through a `data-prop` selector. Selectors point into your data, relative to the root of the current file (typically its front matter) or the [nearest editable ancestor](#how-prop-selectors-compose). The region resolves the selector and re-renders as that value changes; edits write back through the same selector.
|
|
490
|
+
|
|
491
|
+
```astro
|
|
492
|
+
<h4 data-editable="text" data-prop="name">{testimonial.name}</h4>
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Regions can also receive multiple named values with `data-prop-*` attributes. The attribute suffix is the name the value is passed under, and the attribute value is a second selector:
|
|
496
|
+
|
|
497
|
+
```astro
|
|
498
|
+
<img
|
|
499
|
+
data-editable="image"
|
|
500
|
+
data-prop-src="heroImage"
|
|
501
|
+
data-prop-alt="heroImageAlt"
|
|
502
|
+
/>
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Here the image region receives two values: `src` selected from `heroImage`, and `alt` selected from `heroImageAlt`. The names and types of values each region expects are listed in its Options section below.
|
|
506
|
+
|
|
507
|
+
## Value regions
|
|
508
|
+
|
|
509
|
+
Value regions edit a single value. They are the leaves of your page and don't allow any other editable regions to nest within them.
|
|
510
|
+
|
|
511
|
+
### Text
|
|
512
|
+
|
|
513
|
+
Add `data-editable="text"` and a `data-prop` naming the structured data key to edit:
|
|
514
|
+
|
|
515
|
+
```astro
|
|
516
|
+
<h4 data-editable="text" data-prop="name">{testimonial.name}</h4>
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Or use the `editable-text` web component:
|
|
520
|
+
|
|
521
|
+
```astro
|
|
522
|
+
<h4><editable-text data-prop="name">{testimonial.name}</editable-text></h4>
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
#### Editing file content
|
|
526
|
+
|
|
527
|
+
If the element you want to edit is the file's body content (Markdown/HTML content rather than a front matter key) you can use the special `@content` syntax (See [Referencing external data](#referencing-external-data) for more information).
|
|
528
|
+
|
|
529
|
+
```astro
|
|
530
|
+
<div data-editable="text" data-prop="@content">
|
|
531
|
+
<Content />
|
|
532
|
+
</div>
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
By default `@content` refers to the content of the current page. If you want to reference another page's content you can combine it with another external data selector like `@file`.
|
|
536
|
+
|
|
537
|
+
```astro
|
|
538
|
+
<div data-editable="text" data-prop="@file[README.md].@content">
|
|
539
|
+
<Content />
|
|
540
|
+
</div>
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
#### Options
|
|
544
|
+
|
|
545
|
+
| Option | Type | Required | Description |
|
|
546
|
+
| --- | --- | --- | --- |
|
|
547
|
+
| `data-prop` | Selector<string> | Yes | The data to edit, selected value must be a string. |
|
|
548
|
+
| `data-type` | `"span"` \| `"text"` \| `"block"` | No | Controls the editing surface: `span` edits plain text, `text` edits paragraph-level rich text, and `block` edits multi-paragraph rich text. |
|
|
549
|
+
| `data-defer-mount` | Boolean | No | When set, the editor only initializes the region the first time it is clicked, instead of on page load. Defaults to `false`. |
|
|
550
|
+
|
|
551
|
+
The `data-type` default is calculated from the value being edited: regions editing rich text — source regions, `@content`, or values with a Rich Text input configured — default to `block` or `text`, and plain values default to `span`. If the value has no configured input, CloudCannon falls back to the family of the host element (e.g. an `h2` behaves as `text`, a `div` as `block`).
|
|
552
|
+
|
|
553
|
+
### Image
|
|
554
|
+
|
|
555
|
+
Add `data-editable="image"` to (or around) an `<img>` element and bind its attributes with `data-prop-*`:
|
|
556
|
+
|
|
557
|
+
```astro
|
|
558
|
+
<img
|
|
559
|
+
data-editable="image"
|
|
560
|
+
data-prop-src="heroImage"
|
|
561
|
+
data-prop-alt="heroImageAlt"
|
|
562
|
+
data-prop-title="heroImageTitle"
|
|
563
|
+
src={heroImage}
|
|
564
|
+
alt={heroImageAlt}
|
|
565
|
+
title={heroImageTitle}
|
|
566
|
+
/>
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Or use the `editable-image` web component:
|
|
570
|
+
|
|
571
|
+
```astro
|
|
572
|
+
<editable-image data-prop-src="heroImage" data-prop-alt="heroImageAlt">
|
|
573
|
+
<img src={heroImage} alt={heroImageAlt} />
|
|
574
|
+
</editable-image>
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
The region must contain an `<img>` element, either as its child or as the element itself.
|
|
578
|
+
|
|
579
|
+
Clicking an image region opens the *Edit Image* panel in the Visual Editor: upload from your machine, pick an existing file from the repository or a DAM, and edit alt/title details.
|
|
580
|
+
|
|
581
|
+
#### Editing picture elements
|
|
582
|
+
|
|
583
|
+
Image regions can also wrap a `<picture>` element. Put the region attributes on the `<picture>`; when the image changes, CloudCannon updates the preview URL on the `<source>` elements as well as the `<img>`:
|
|
584
|
+
|
|
585
|
+
```astro
|
|
586
|
+
<picture data-editable="image" data-prop-src="heroImage" data-prop-alt="heroImageAlt">
|
|
587
|
+
<source srcset={heroImageAvif} type="image/avif" />
|
|
588
|
+
<img src={heroImage} alt={heroImageAlt} />
|
|
589
|
+
</picture>
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
#### Options
|
|
593
|
+
|
|
594
|
+
| Option | Type | Required | Description |
|
|
595
|
+
| --- | --- | --- | --- |
|
|
596
|
+
| `data-prop` | Selector<{ src: string, alt?: string, title?: string}> | No | Selects an object holding the image details. Each image attribute binds to `<selector>.src`, `<selector>.alt`, and `<selector>.title`. |
|
|
597
|
+
| `data-prop-src` | Selector<string> | No | Selects the data for the image source. |
|
|
598
|
+
| `data-prop-alt` | Selector<string> | No | Selects the data for the alt text. |
|
|
599
|
+
| `data-prop-title` | Selector<string> | No | Selects the data for the title text. |
|
|
600
|
+
|
|
601
|
+
At least one selector is required. When you bind an object with `data-prop`, its value should only contain the `src`, `alt`, and `title` keys; anything else raises an error card suggesting you remove the extra binding.
|
|
602
|
+
|
|
603
|
+
## Structural regions
|
|
604
|
+
|
|
605
|
+
Structural regions define the shape of your page rather than a single value. Unlike value editable regions, structural editable regions can have other editable regions nested within them and they can compose with each other.
|
|
606
|
+
|
|
607
|
+
### Components
|
|
608
|
+
|
|
609
|
+
A component region binds a piece of markup to a **registered** component that the editor can re-render as its data changes.
|
|
610
|
+
|
|
611
|
+
```astro
|
|
612
|
+
<editable-component data-component="cta" data-prop="cta">
|
|
613
|
+
<CTA {...cta} />
|
|
614
|
+
</editable-component>
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
#### Options
|
|
618
|
+
|
|
619
|
+
| Option | Type | Required | Description |
|
|
620
|
+
| --- | --- | --- | --- |
|
|
621
|
+
| `data-component` | String | Yes | The key the component was registered under (in Hugo, the partial path with extension, e.g. `card.html`). |
|
|
622
|
+
| `data-prop` | Selector<Object> | No | The data passed to the component as props. |
|
|
623
|
+
|
|
624
|
+
Inside the component's own file, add more editable regions for its text, images, and nested arrays. When a user edits the component's data in the Visual Editor, the registered renderer re-renders the component and the result is updated in place.
|
|
625
|
+
|
|
626
|
+
### Arrays and array items
|
|
627
|
+
|
|
628
|
+
An array region defines a region of markup that is built from a list of repeated elements. An array item region defines the boundary of a single element in that array. Array and array item regions always work as a pair, and as a rule of thumb they will be positioned on either side of the loop in your template.
|
|
629
|
+
|
|
630
|
+
```astro
|
|
631
|
+
<ul data-editable="array" data-prop="testimonials">
|
|
632
|
+
{testimonials.map((testimonial) => (
|
|
633
|
+
<li data-editable="array-item">
|
|
634
|
+
<h4 data-editable="text" data-prop="name">{testimonial.name}</h4>
|
|
635
|
+
<p data-editable="text" data-prop="quote">{testimonial.quote}</p>
|
|
636
|
+
</li>
|
|
637
|
+
))}
|
|
638
|
+
</ul>
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
#### Adding items to an array
|
|
642
|
+
|
|
643
|
+
When a user adds an item in the Visual Editor, the editor has to invent markup for the new item. How it does that depends on how the array is set up:
|
|
644
|
+
|
|
645
|
+
##### Homogeneous arrays with a component
|
|
646
|
+
|
|
647
|
+
If the array names a component with `data-component`, every item is rendered by that component. A new item is created as a bare array item, and the registered component renders it from the item's data alone; no template markup is needed.
|
|
648
|
+
|
|
649
|
+
```astro
|
|
650
|
+
<ul data-editable="array" data-prop="testimonials" data-component="testimonial">
|
|
651
|
+
{testimonials.map(() => (
|
|
652
|
+
<li data-editable="array-item" data-component="testimonial">
|
|
653
|
+
<Testimonial />
|
|
654
|
+
</li>
|
|
655
|
+
))}
|
|
656
|
+
</ul>
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
##### Homogeneous arrays with a template
|
|
660
|
+
|
|
661
|
+
A `<template>` element inside the array region is a blueprint for new items. A template without a `data-id` is the blueprint for every new item:
|
|
662
|
+
|
|
663
|
+
```html
|
|
664
|
+
<ul data-editable="array" data-prop="features">
|
|
665
|
+
<template>
|
|
666
|
+
<li>
|
|
667
|
+
<h4 data-editable="text" data-prop="title"></h4>
|
|
668
|
+
<p data-editable="text" data-prop="description"></p>
|
|
669
|
+
</li>
|
|
670
|
+
</template>
|
|
671
|
+
</ul>
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Multi-root templates are automatically wrapped in an `editable-array-item`. With neither a template nor a component, the editor clones the markup of an existing item instead; if the array is empty and has neither, you get an error card naming what to add.
|
|
675
|
+
|
|
676
|
+
##### Heterogeneous arrays with an id key and component key
|
|
677
|
+
|
|
678
|
+
When items have different shapes, each item names its own component. The array's `data-id-key` and `data-component-key` tell the editor which item keys hold the id and the component name; see [page building](#page-building) below for a full setup. A new item is created as a bare array item, and its component renders it:
|
|
679
|
+
|
|
680
|
+
```html
|
|
681
|
+
<main data-editable="array" data-prop="contentBlocks" data-component-key="_name">
|
|
682
|
+
<section data-editable="array-item" data-id="HeroBlock" data-component="HeroBlock">
|
|
683
|
+
<h2 data-editable="text" data-prop="title"></h2>
|
|
684
|
+
</section>
|
|
685
|
+
</main>
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Each item's `_name` value is used as both its id and its component, so a new item named `HeroBlock` is rendered by the registered `HeroBlock` component.
|
|
689
|
+
|
|
690
|
+
##### Heterogeneous arrays with an id key and template
|
|
691
|
+
|
|
692
|
+
A keyed `<template data-id="…">` is the blueprint for new items with that id; each new item picks its shape from the right template. The item's id comes from the array's `data-id-key`; a plain `<template>` (no `data-id`) is the fallback for ids without a keyed template.
|
|
693
|
+
|
|
694
|
+
```html
|
|
695
|
+
<ul data-editable="array" data-prop="blocks" data-id-key="type">
|
|
696
|
+
<template data-id="hero">
|
|
697
|
+
<section class="hero">
|
|
698
|
+
<h2 data-editable="text" data-prop="title"></h2>
|
|
699
|
+
</section>
|
|
700
|
+
</template>
|
|
701
|
+
<template data-id="stats">
|
|
702
|
+
<section class="stats"><!-- ... --></section>
|
|
703
|
+
</template>
|
|
704
|
+
</ul>
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
#### Special props
|
|
708
|
+
|
|
709
|
+
Two selectors are provided by the array structure itself rather than your data:
|
|
710
|
+
|
|
711
|
+
- `@length` — provided by array regions; the item count.
|
|
712
|
+
- `@index` — provided by each array item; the item's position.
|
|
713
|
+
|
|
714
|
+
Reference them like any other selector, with `data-prop="@length"` or `data-prop="@index"`.
|
|
715
|
+
|
|
716
|
+
#### Options
|
|
717
|
+
|
|
718
|
+
Array region:
|
|
719
|
+
|
|
720
|
+
| Option | Type | Required | Description |
|
|
721
|
+
| --- | --- | --- | --- |
|
|
722
|
+
| `data-prop` | Selector<Array> | Yes | The array to edit. |
|
|
723
|
+
| `data-id-key` | String | No | The item key that holds each item's unique identifier. See [page building](#page-building). |
|
|
724
|
+
| `data-component` | String | No | The registered component to render items that don't resolve a component from the component key. |
|
|
725
|
+
| `data-component-key` | String | No | The item key that holds the name of the registered component to render each item. |
|
|
726
|
+
| `data-direction` | `"row"` \| `"column"` \| `"row-reverse"` \| `"column-reverse"` | No | The direction the items are laid out in, so item controls and drag-and-drop work correctly. Defaults to the element's flex direction when it is a flex container, otherwise `column`. |
|
|
727
|
+
|
|
728
|
+
Array item region:
|
|
729
|
+
|
|
730
|
+
| Option | Type | Required | Description |
|
|
731
|
+
| --- | --- | --- | --- |
|
|
732
|
+
| `data-id` | String | No | The item's unique identifier, used to match items to `<template>` blueprints and reconcile the DOM as items move. Usually populated from the array's `data-id-key`. |
|
|
733
|
+
| `data-component` | String | No | The registered component used to render this item. Usually populated from the array's `data-component-key`. |
|
|
734
|
+
|
|
735
|
+
The array region assigns each item its `data-prop` (the item's index) at hydration; you don't set it yourself. Array item regions must be the immediate child of the array element — keep the pair as close together as possible.
|
|
736
|
+
|
|
737
|
+
With the regions in place, editors can add, duplicate, delete, reorder, and drag items — with the preview updating live.
|
|
738
|
+
|
|
739
|
+
### How prop selectors compose
|
|
740
|
+
|
|
741
|
+
Every `data-prop` selector is relative to the value of the nearest editable ancestor. The chain composes from the top of the page down:
|
|
742
|
+
|
|
743
|
+
```astro
|
|
744
|
+
<main data-editable="array" data-prop="blocks">
|
|
745
|
+
{blocks.map((block, i) => (
|
|
746
|
+
<section data-editable="array-item">
|
|
747
|
+
<h2 data-editable="text" data-prop="title">{block.title}</h2>
|
|
748
|
+
<img data-editable="image" data-prop-src="image" src={block.image} />
|
|
749
|
+
</section>
|
|
750
|
+
))}
|
|
751
|
+
</main>
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
Reading the regions inside-out:
|
|
755
|
+
|
|
756
|
+
- The array region holds `blocks` — the whole array.
|
|
757
|
+
- Each array item is handed one entry: the array gives its items selectors `blocks.0`, `blocks.1`, …
|
|
758
|
+
- The text region inside the item resolves `title` against that value: `blocks.0.title`.
|
|
759
|
+
|
|
760
|
+
Writes travel the same chain in reverse. Editing the `<h2>` dispatches a change event for `title`; each ancestor prepends its own segment until the root region executes `blocks.0.title = "New value"` against the underlying file. That's also why the [best practice is to add regions parent-first](https://cloudcannon.com/documentation/developer-guides/set-up-visual-editing/an-overview-of-editable-regions/): a region nested inside an array or component can't resolve its selector until the ancestors exist.
|
|
761
|
+
|
|
762
|
+
## Page building
|
|
763
|
+
|
|
764
|
+
Combining the heterogeneous array setup with registered components gives you page building: each item in the array is a content block, and editors can add, reorder, and edit blocks to compose the page.
|
|
765
|
+
|
|
766
|
+
```astro
|
|
767
|
+
<main
|
|
768
|
+
data-editable="array"
|
|
769
|
+
data-prop="contentBlocks"
|
|
770
|
+
data-id-key="_name"
|
|
771
|
+
data-component-key="_name"
|
|
772
|
+
>
|
|
773
|
+
{contentBlocks.map((block) => {
|
|
774
|
+
const Component = components[block._name];
|
|
775
|
+
return (
|
|
776
|
+
<section data-editable="array-item" data-id={block._name} data-component={block._name}>
|
|
777
|
+
<Component {...block} />
|
|
778
|
+
</section>
|
|
779
|
+
);
|
|
780
|
+
})}
|
|
781
|
+
</main>
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
With front matter like:
|
|
785
|
+
|
|
786
|
+
```yaml
|
|
787
|
+
contentBlocks:
|
|
788
|
+
- _name: HeroBlock
|
|
789
|
+
title: We're on a mission
|
|
790
|
+
description: Lorem ipsum dolor sit amet…
|
|
791
|
+
- _name: StatsBlock
|
|
792
|
+
stats:
|
|
793
|
+
- figure: $200m
|
|
794
|
+
text: Venture capital raised
|
|
795
|
+
- figure: 40+
|
|
796
|
+
text: Amazing team members
|
|
797
|
+
- _name: ContactBlock
|
|
798
|
+
text: Want to get in contact with us?
|
|
799
|
+
button: Click here
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
Register each block component, and editors can add any block type, reorder them, and edit each one's contents. Keeping `_name` as both the id and the component key is common and convenient, but they can be separate keys. If both keys are the same then you only need to specify `data-component-key` and `data-id-key` will default to the same value.
|
|
803
|
+
|
|
804
|
+
## Referencing external data
|
|
805
|
+
|
|
806
|
+
`data-prop` selectors are relative to the current file (or the nearest editable ancestor). To point at data that lives somewhere else, start the selector with an `@` reference:
|
|
807
|
+
|
|
808
|
+
| Reference | Resolves to |
|
|
809
|
+
| --- | --- |
|
|
810
|
+
| `@collections[blog]` | The `blog` collection — usable directly as an array region's value |
|
|
811
|
+
| `@data[nav]` | The `nav` dataset (data files managed outside the page) |
|
|
812
|
+
| `@file[about.md]` | Another file's structured data |
|
|
813
|
+
| `@file[about.md].@content` | Another file's body content |
|
|
814
|
+
| `@content` | The current file's body content |
|
|
815
|
+
|
|
816
|
+
```astro
|
|
817
|
+
<ul data-editable="array" data-prop="@collections[blog]">
|
|
818
|
+
<li data-editable="array-item">
|
|
819
|
+
<h4 data-editable="text" data-prop="title" />
|
|
820
|
+
</li>
|
|
821
|
+
</ul>
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
Notes:
|
|
825
|
+
|
|
826
|
+
- `@content` on a text region gives you a rich text editor over the file's Markdown/HTML body — see [Editing file content](#editing-file-content) for details and examples.
|
|
827
|
+
- Unqualified selectors after an `@` reference (e.g. `@file[about.md].seo.title`) continue into the resolved object.
|
|
828
|
+
|
|
829
|
+
## Literal props
|
|
830
|
+
|
|
831
|
+
For values that don't live in any file at all, `data-literal-prop-*` attributes pass literal values to a region. The attribute suffix is the name the value is passed under (see [passing values to editable regions](#passing-values-to-editable-regions)):
|
|
832
|
+
|
|
833
|
+
```html
|
|
834
|
+
<editable-component
|
|
835
|
+
data-component="author-card"
|
|
836
|
+
data-literal-prop-name="C. Kent"
|
|
837
|
+
data-literal-prop-role="Support engineer"
|
|
838
|
+
/>
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
Here the `author-card` component receives `{ name: "C. Kent", role: "Support engineer" }`.
|
|
842
|
+
|
|
843
|
+
Values that parse as JSON are passed as structured values:
|
|
844
|
+
|
|
845
|
+
```html
|
|
846
|
+
<editable-component
|
|
847
|
+
data-component="author-card"
|
|
848
|
+
data-literal-prop-social='{"twitter": "@cloudcannon"}'
|
|
849
|
+
/>
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
Here the component receives `{ social: { twitter: "@cloudcannon" } }`. Values that don't parse as JSON are passed as plain strings.
|
|
853
|
+
|
|
854
|
+
## Source regions
|
|
855
|
+
|
|
856
|
+
Value and structural regions all edit **structured data**, and your templates re-render it. Source regions are the exception: they edit the **hard-coded markup of the template file itself**.
|
|
857
|
+
|
|
858
|
+
### How they work
|
|
859
|
+
|
|
860
|
+
```astro
|
|
861
|
+
<p
|
|
862
|
+
data-editable="source"
|
|
863
|
+
data-path="src/pages/about.astro"
|
|
864
|
+
data-key="about-intro"
|
|
865
|
+
>
|
|
866
|
+
We've been building websites since 2014.
|
|
867
|
+
</p>
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
When the file changes in the editor, the runtime fetches the raw source of the file, finds the element by locating its `data-key` attribute, and then edits its content directly in the source file.
|
|
871
|
+
|
|
872
|
+
### Limitations
|
|
873
|
+
|
|
874
|
+
- **Static markup only.** The region edits literal HTML in the file. If the element contains any templating you must have a matching Snippet configured, otherwise it cannot be safely parsed by the editor.
|
|
875
|
+
- **`data-key` values must be unique within a file**, and the attribute must survive into the built output.
|
|
876
|
+
- Because source editable regions edit the source directly, edits from one region will update all other places where that file is referenced. Any dynamic content that isn't configured as a Snippet will be overwritten with static content.
|
|
877
|
+
|
|
878
|
+
### When to use them
|
|
879
|
+
|
|
880
|
+
Use a source region when there is no structured data key behind the text. For example, landing pages and standalone templates with hard-coded copy, or a call-to-action baked into a layout. It's the escape hatch that makes hard-coded HTML editable without restructuring your templates around a data file.
|
|
881
|
+
|
|
882
|
+
If you *can* move the value into front matter or a data file, prefer a text region: data-driven regions re-render live and keep your content where content belongs.
|
|
883
|
+
|
|
884
|
+
### Options
|
|
885
|
+
|
|
886
|
+
| Option | Type | Required | Description |
|
|
887
|
+
| --- | --- | --- | --- |
|
|
888
|
+
| `data-path` | String | Yes | The template file the text lives in, relative to the site root. A leading `/` is optional. This is a file path, not a selector. |
|
|
889
|
+
| `data-key` | String | Yes | The name identifying this region within the file. Must be unique among all `data-key` values in the same file. |
|
|
890
|
+
| `data-type` | `"span"` \| `"text"` \| `"block"` | No | Controls the rich text editing surface, as with [text regions](#text). Source content is edited as HTML. |
|