@cloudcannon/editable-regions 0.0.17 → 0.0.18
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/helpers/cloudcannon.mjs +7 -22
- package/integrations/astro/astro-integration.mjs +1 -4
- package/integrations/astro/index.mjs +15 -41
- package/integrations/astro/modules/assets.js +1 -4
- package/integrations/astro/modules/content.js +2 -11
- package/integrations/astro/react-renderer.mjs +4 -5
- package/integrations/eleventy/browser/collect-config.mjs +188 -0
- package/integrations/eleventy/browser/index.mjs +9 -0
- package/integrations/eleventy/browser/liquid-builtins.mjs +306 -0
- package/integrations/eleventy/browser/liquid-render.mjs +216 -0
- package/integrations/eleventy/index.cjs +1 -0
- package/integrations/eleventy/index.mjs +582 -0
- package/integrations/liquid/README.md +588 -0
- package/integrations/liquid/errors.mjs +39 -0
- package/integrations/liquid/fs.mjs +25 -74
- package/integrations/liquid/globals.mjs +209 -0
- package/integrations/liquid/include-with-tag.mjs +84 -0
- package/integrations/liquid/index.mjs +163 -170
- package/integrations/liquid/logger.mjs +15 -78
- package/integrations/liquid/page-map.mjs +32 -0
- package/integrations/liquid/shortcodes.mjs +62 -99
- package/integrations/react.mjs +5 -9
- package/package.json +18 -7
- package/types/eleventy.d.cts +20 -0
- package/types/eleventy.d.ts +81 -0
- package/types/liquid.d.ts +15 -12
- package/integrations/eleventy.mjs +0 -294
- package/integrations/liquid/11ty-filters.mjs +0 -69
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
# Liquid live-editing runtime
|
|
2
|
+
|
|
3
|
+
Browser-side Liquid engine used by the CloudCannon Visual Editor. The Eleventy
|
|
4
|
+
plugin (`integrations/eleventy/index.mjs`) generates a `register-components.js` bundle at
|
|
5
|
+
build time; this directory is what that bundle pulls in.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [What this is for](#what-this-is-for)
|
|
10
|
+
- [Install and configure](#install-and-configure)
|
|
11
|
+
- [Plugin options at a glance](#plugin-options-at-a-glance)
|
|
12
|
+
- [How it fits together](#how-it-fits-together)
|
|
13
|
+
- [Globals](#globals)
|
|
14
|
+
- [`page` properties](#page-properties)
|
|
15
|
+
- [Custom globals](#custom-globals)
|
|
16
|
+
- [Eleventy global](#eleventy-global)
|
|
17
|
+
- [`pkg` global](#pkg-global)
|
|
18
|
+
- [Filters](#filters)
|
|
19
|
+
- [Adding a custom filter](#adding-a-custom-filter)
|
|
20
|
+
- [Overriding a built-in](#overriding-a-built-in)
|
|
21
|
+
- [Shortcodes and paired shortcodes](#shortcodes-and-paired-shortcodes)
|
|
22
|
+
- [Adding a custom shortcode](#adding-a-custom-shortcode)
|
|
23
|
+
- [Tags](#tags)
|
|
24
|
+
- [Built-in tags](#built-in-tags)
|
|
25
|
+
- [RenderPlugin shims](#renderplugin-shims)
|
|
26
|
+
- [Component resolution](#component-resolution)
|
|
27
|
+
- [Virtual filesystem](#virtual-filesystem)
|
|
28
|
+
- [Error enhancement](#error-enhancement)
|
|
29
|
+
- [Limitations and fallbacks](#limitations-and-fallbacks)
|
|
30
|
+
- [Things that don't work in live editing](#things-that-dont-work-in-live-editing)
|
|
31
|
+
- [Patterns](#patterns)
|
|
32
|
+
|
|
33
|
+
## What this is for
|
|
34
|
+
|
|
35
|
+
This integration powers **live-editing of components** inside the CloudCannon
|
|
36
|
+
Visual Editor. The "component" is the unit: a Liquid partial (e.g.
|
|
37
|
+
`_includes/card.liquid`) that the editor re-renders client-side as the user
|
|
38
|
+
edits its data, without round-tripping through Eleventy.
|
|
39
|
+
|
|
40
|
+
It is **not** a full client-side replacement for Eleventy. Pages are still
|
|
41
|
+
built by Eleventy and served as static HTML; this runtime only re-renders the
|
|
42
|
+
components the editor swaps in. Anything belonging to the page lifecycle
|
|
43
|
+
(permalinks, layouts, build filters, output paths) is shimmed approximately or
|
|
44
|
+
not implemented — see "Limitations and fallbacks".
|
|
45
|
+
|
|
46
|
+
## Install and configure
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm install @cloudcannon/editable-regions
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Wire the plugin into your `eleventy.config.mjs`. The minimal case is one
|
|
53
|
+
line — Liquid is the plugin's default language and is enabled implicitly:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import editableRegions from "@cloudcannon/editable-regions/eleventy";
|
|
57
|
+
|
|
58
|
+
export default function (eleventyConfig) {
|
|
59
|
+
// your existing filters, shortcodes, collections, etc.
|
|
60
|
+
|
|
61
|
+
eleventyConfig.addPlugin(editableRegions);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
dir: { input: "src", includes: "_includes", output: "_site" },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
To customise — environment variables, output path, or Liquid-specific
|
|
70
|
+
options:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
eleventyConfig.addPlugin(editableRegions, {
|
|
74
|
+
liquid: {
|
|
75
|
+
extensions: [".liquid"],
|
|
76
|
+
// see "Adding custom …" sections below for filter / shortcode / tag overrides
|
|
77
|
+
},
|
|
78
|
+
globals: { // optional — see "Custom globals"
|
|
79
|
+
env: { API_BASE: process.env.API_BASE },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`liquid` accepts `true` (defaults), `false` (disable Liquid live editing),
|
|
85
|
+
or an options object. Future languages will follow the same shape but
|
|
86
|
+
default to off — users will opt in via e.g. `nunjucks: true`.
|
|
87
|
+
|
|
88
|
+
After every build, the plugin emits `register-components.js` into your
|
|
89
|
+
output directory. The filename and location are configurable via the
|
|
90
|
+
`output` plugin option (see the table below) — the default sits next to
|
|
91
|
+
the rest of your built assets so it's reachable as
|
|
92
|
+
`/register-components.js`.
|
|
93
|
+
|
|
94
|
+
Load it on every page the Visual Editor will render against, guarded on
|
|
95
|
+
the editor's runtime flag so production pages don't pay the cost outside
|
|
96
|
+
the editor:
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<script>
|
|
100
|
+
if (window.inEditorMode) {
|
|
101
|
+
import("/register-components.js").catch((error) => {
|
|
102
|
+
console.warn("Failed to load CloudCannon component registration:", error);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
</script>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`window.inEditorMode` is set to `true` by the CloudCannon Visual Editor
|
|
109
|
+
before page scripts run; outside the editor it's `undefined`, so the
|
|
110
|
+
dynamic import never fires. If you'd rather always load the bundle
|
|
111
|
+
(useful while iterating locally), a plain `<script src="/register-components.js" defer>`
|
|
112
|
+
works too.
|
|
113
|
+
|
|
114
|
+
### Plugin options at a glance
|
|
115
|
+
|
|
116
|
+
| Option | Purpose |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `output` | Where to write the bundle. Defaults to `register-components.js` inside Eleventy's `dir.output`. |
|
|
119
|
+
| `verbose` | Enable verbose browser logging. |
|
|
120
|
+
| `globals` | Extra globals to expose to editor-rendered templates (JSON-serialisable). See "Custom globals". |
|
|
121
|
+
| `liquid.extensions` | Template file extensions to bundle. Defaults to `[".liquid", ".html"]`. |
|
|
122
|
+
| `liquid.componentDirs` | Directories to walk for component templates. Defaults to `[directories.includes, directories.input]`. |
|
|
123
|
+
| `liquid.ignoreDirectories` | Directory names to skip when walking. Defaults to `[directories.output, "node_modules"]`. |
|
|
124
|
+
| `liquid.components` | Map of component name → module path. Wins over the filesystem-resolution proxy. |
|
|
125
|
+
| `liquid.filters` | Map of filter name → module path. Browser-side override. See "Adding a custom filter". |
|
|
126
|
+
| `liquid.shortcodes` | Map of shortcode name → module path. Browser-side override. See "Adding a custom shortcode". |
|
|
127
|
+
| `liquid.pairedShortcodes` | Same as `shortcodes`, for paired shortcodes. |
|
|
128
|
+
| `liquid.tags` | Map of tag name → factory module path. Browser-side override. Tags auto-mirror from the config like filters/shortcodes; use this only for a tag that can't run in the browser as written. |
|
|
129
|
+
| `liquid.configPath` | Path to the Eleventy config file to import and replay for the auto-mirror, relative to the project root. Defaults to the first of 11ty's standard names that exists (`.eleventy.js`, `eleventy.config.{js,mjs,cjs}`). Set only if you run Eleventy with a non-default `--config`. |
|
|
130
|
+
| `liquid.browserStub` | Extra bare module specifiers to stub out of the browser bundle, on top of the 11ty toolchain and Node built-ins (always stubbed). Use when the config imports a native/Node-only package (e.g. `sharp`) that no browser-bound helper actually calls. |
|
|
131
|
+
|
|
132
|
+
## How it fits together
|
|
133
|
+
|
|
134
|
+
After every build, the plugin emits a single `register-components.js` bundle
|
|
135
|
+
that the Visual Editor loads. Two things are picked up at build time and wired
|
|
136
|
+
into that bundle:
|
|
137
|
+
|
|
138
|
+
- **Filters, shortcodes, and tags** — auto-mirrored from your Eleventy config
|
|
139
|
+
(see "Filters" below).
|
|
140
|
+
- **Components** — every template under the configured component directories
|
|
141
|
+
(`liquid.componentDirs`, defaulting to `dir.includes` and `dir.input`),
|
|
142
|
+
matching `liquid.extensions`.
|
|
143
|
+
|
|
144
|
+
In the browser, the bundle instantiates a shared Liquid engine, registers
|
|
145
|
+
everything, and resolves each component on demand via `{% include %}`. See
|
|
146
|
+
"Component resolution" for how component names map to templates.
|
|
147
|
+
|
|
148
|
+
## Globals
|
|
149
|
+
|
|
150
|
+
Globals are passed to `new Liquid({ globals })` inside `createSharedLiquidEngine`:
|
|
151
|
+
|
|
152
|
+
| Global | Status | Notes |
|
|
153
|
+
| --- | --- | --- |
|
|
154
|
+
| `collections` | Implemented | `Proxy` that lazily resolves `collections.foo` to an array of items via the Visual Editor API. Items shaped roughly like Eleventy's: `{ url, inputPath, data }`. |
|
|
155
|
+
| `ENV_CLIENT` | Implemented | Always `true` in this bundle. Templates can branch on it to opt out of build-only logic. |
|
|
156
|
+
| `page` | Partial | `Proxy` backed by `CloudCannon.currentFile()`. See below for which properties are supported. |
|
|
157
|
+
| custom globals | Opt-in | Whatever you pass via `pluginOptions.globals` (e.g. an `env` object), embedded at build time. See "Custom globals" below. |
|
|
158
|
+
| `eleventy` | Partial | Static object built at build time. See "Eleventy global" below. |
|
|
159
|
+
| `pkg` | Implemented | Project `package.json`, mirrored verbatim. See "`pkg` global" below. |
|
|
160
|
+
|
|
161
|
+
### `page` properties
|
|
162
|
+
|
|
163
|
+
Each property access returns a Promise; liquidjs awaits as part of normal
|
|
164
|
+
expression evaluation. Source is `CloudCannon.currentFile()` and its front
|
|
165
|
+
matter (`file.data.get()`).
|
|
166
|
+
|
|
167
|
+
| Property | Source | Notes |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| `inputPath` | `currentFile().path` | Project-relative; may differ slightly from Eleventy's `./input-dir/...` form. |
|
|
170
|
+
| `fileSlug` | derived from `path` | Basename minus extension. |
|
|
171
|
+
| `filePathStem` | derived from `path` | Full path minus extension, with a leading `/`. |
|
|
172
|
+
| `outputFileExtension` | constant `"html"` | We don't model custom output extensions. |
|
|
173
|
+
| `date` | front matter `date` | Coerced to a `Date`. Returns `undefined` if absent or unparseable; we can't see file mtime / git history from the browser. |
|
|
174
|
+
| `url` | live `permalink`, else build-time page map, else folder-style derivation | Priority: a *literal* front-matter `permalink` (captures editor-time edits) → build-time page-map lookup → 11ty's folder-style default. A `permalink` containing template syntax (e.g. `"/{{ page.date \| date: '%Y/%m/%d' }}/"`) is skipped here and resolved via the page map, which holds 11ty's already-rendered value. |
|
|
175
|
+
| `outputPath` | live `permalink` joined with `directories.output`, else build-time page map, else folder-style default joined with `directories.output` | Same priority hierarchy as `url` (templated permalinks likewise fall through to the page map). Build-map lookup uses 11ty's exact `outputPath` (so `index.html` joining matches what 11ty wrote). Returns `undefined` only if neither the map nor `registerEleventyData` have run. |
|
|
176
|
+
| `templateSyntax` | — | Unimplemented. |
|
|
177
|
+
| `lang` | — | Unimplemented (would need the i18n plugin's runtime state). |
|
|
178
|
+
|
|
179
|
+
### Custom globals
|
|
180
|
+
|
|
181
|
+
11ty doesn't expose `process.env` to templates — global data reaches them by
|
|
182
|
+
name instead (a `_data/env.js` file becomes `{{ env.* }}`, or
|
|
183
|
+
`addGlobalData("env", …)`). The live-editing runtime doesn't auto-load your
|
|
184
|
+
global data, so anything a component reads that isn't `page` / `collections` /
|
|
185
|
+
`eleventy` / `pkg` has to be passed in explicitly via `pluginOptions.globals`.
|
|
186
|
+
Mirror whatever your build already exposes, so the editor and build agree:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
const env = { API_BASE: process.env.API_BASE };
|
|
190
|
+
|
|
191
|
+
eleventyConfig.addGlobalData("env", env); // server-side build
|
|
192
|
+
|
|
193
|
+
eleventyConfig.addPlugin(editableRegions, {
|
|
194
|
+
globals: { env }, // live editing
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Templates then read it by name, identically in both places:
|
|
199
|
+
|
|
200
|
+
```liquid
|
|
201
|
+
<a href="{{ env.API_BASE }}">…</a>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The object is embedded into the bundle as a JSON literal at build time, so
|
|
205
|
+
values must be JSON-serialisable (no functions). The built-in globals
|
|
206
|
+
(`page`, `collections`, `eleventy`, `pkg`) are applied separately and win on a
|
|
207
|
+
name collision.
|
|
208
|
+
|
|
209
|
+
> ⚠️ **Never include secrets.** Anything in `globals` is embedded verbatim
|
|
210
|
+
> into the static JS bundle the browser downloads. Treat it like Vite's
|
|
211
|
+
> `PUBLIC_` or Next's `NEXT_PUBLIC_` convention: public-by-design only. Keep
|
|
212
|
+
> API keys, tokens, signing secrets, and database URLs out of it.
|
|
213
|
+
|
|
214
|
+
### Eleventy global
|
|
215
|
+
|
|
216
|
+
A static `eleventy` object is registered alongside `collections` and `page`,
|
|
217
|
+
built once at build time:
|
|
218
|
+
|
|
219
|
+
| Property | Source | Notes |
|
|
220
|
+
| --- | --- | --- |
|
|
221
|
+
| `eleventy.version` | resolved from `@11ty/eleventy/package.json` | Falls back to `"unknown"` if Eleventy can't be resolved (so the bundle still builds). |
|
|
222
|
+
| `eleventy.generator` | `"Eleventy v" + version` | Useful in feed/sitemap templates. |
|
|
223
|
+
| `eleventy.env.runMode` | hardcoded `"serve"` | We're not in any of 11ty's real run modes; "serve" is the dev-mode analogue. Templates branching on `runMode` see this as the "live" path. |
|
|
224
|
+
| `eleventy.env.source` | hardcoded `"cli"` | Same idea — pick the most-common analogue so branches don't go down a build-only path. |
|
|
225
|
+
| `eleventy.env.config` / `env.root` | — | Deliberately omitted. These are absolute filesystem paths and have no place in client JS. |
|
|
226
|
+
| `eleventy.directories` | from the build's `directories` payload | `{ input, includes, data, output }`. |
|
|
227
|
+
| `eleventy.serverless` | — | Deprecated upstream, not shipped. |
|
|
228
|
+
|
|
229
|
+
### `pkg` global
|
|
230
|
+
|
|
231
|
+
11ty exposes the project's `package.json` as the `pkg` global by default
|
|
232
|
+
(`config.keys.package = "pkg"`). We mirror it verbatim — `pkg.name`,
|
|
233
|
+
`pkg.version`, `pkg.description`, `pkg.author`, `pkg.homepage`, and any other
|
|
234
|
+
top-level fields the consumer has set are available in editable templates the
|
|
235
|
+
same way they are server-side.
|
|
236
|
+
|
|
237
|
+
If `package.json` is missing or malformed at build time, the bundle skips
|
|
238
|
+
`registerPkg` entirely and `pkg` is `undefined` in templates.
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
## Filters
|
|
242
|
+
|
|
243
|
+
Filters come from three sources, resolved in order so later sources win on
|
|
244
|
+
name collision: **built-ins**, **auto-mirrored**, then **overrides**.
|
|
245
|
+
|
|
246
|
+
1. **Built-ins.** Browser-safe reimplementations of common Eleventy built-ins:
|
|
247
|
+
`slugify`/`slug`, `url`, the date filters, `getNewestCollectionItemDate`,
|
|
248
|
+
the four collection-item filters, and `log`. `inputPathToUrl` is backed by
|
|
249
|
+
the build-time page map, so it resolves the correct URL for any file in the
|
|
250
|
+
last build, including computed permalinks. `renderContent`
|
|
251
|
+
is a real shim (see "RenderPlugin shims"). Filters that depend on
|
|
252
|
+
build-time-only state we don't model (`htmlBaseUrl`, `serverlessUrl`) are
|
|
253
|
+
warn-once pass-throughs that return their input unchanged.
|
|
254
|
+
|
|
255
|
+
2. **Auto-mirrored from your Eleventy config.** The bundle imports your real
|
|
256
|
+
config and replays it in the browser, capturing every `addFilter` /
|
|
257
|
+
`addAsyncFilter` / `addLiquidFilter` call. Because the config is bundled
|
|
258
|
+
(not serialized), each function keeps its closures and imports. A filter
|
|
259
|
+
that depends on Eleventy build-time state (`this.ctx`) or calls a Node API
|
|
260
|
+
at render time will throw when invoked in the browser — the signal to add
|
|
261
|
+
an override.
|
|
262
|
+
|
|
263
|
+
3. **Overrides** (`pluginOptions.liquid.filters`). A map from filter name to
|
|
264
|
+
module path. Two reasons to use this:
|
|
265
|
+
- **A mirrored filter throws at render time** — supply a browser-safe
|
|
266
|
+
replacement here.
|
|
267
|
+
- **You're overriding a built-in name.** The auto-mirror skips built-in
|
|
268
|
+
names to protect our browser ports, so an
|
|
269
|
+
`eleventyConfig.addFilter("url", …)` won't reach live editing unless you
|
|
270
|
+
also register it here.
|
|
271
|
+
|
|
272
|
+
### Adding a custom filter
|
|
273
|
+
|
|
274
|
+
For most filters you don't need to do anything — registering with Eleventy
|
|
275
|
+
the normal way is enough; the auto-mirror picks it up.
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
// eleventy.config.mjs
|
|
279
|
+
eleventyConfig.addFilter("shout", (s) => String(s).toUpperCase());
|
|
280
|
+
// → available in live editing as `{{ "hi" | shout }}` automatically
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
If your filter touches `this.ctx`, `process`, `require`, `__dirname`, or a
|
|
284
|
+
closed-over Node module, the auto-mirror will ship it but it'll throw at
|
|
285
|
+
render time in the browser. Surface the actionable path by adding an
|
|
286
|
+
override:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
// eleventy.config.mjs
|
|
290
|
+
import { readFileSync } from "node:fs";
|
|
291
|
+
eleventyConfig.addFilter("siteConfig", (key) => {
|
|
292
|
+
// Reads from disk — fine server-side, throws in the browser.
|
|
293
|
+
return JSON.parse(readFileSync("./site-config.json", "utf8"))[key];
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
eleventyConfig.addPlugin(editableRegions, {
|
|
297
|
+
liquid: {
|
|
298
|
+
filters: {
|
|
299
|
+
siteConfig: "./live-editing-overrides/site-config.mjs",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
// live-editing-overrides/site-config.mjs
|
|
307
|
+
import config from "../site-config.json"; // esbuild inlines this at build time
|
|
308
|
+
export default function siteConfig(key) {
|
|
309
|
+
return config[key];
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The override module's default export is registered against the live-editing
|
|
314
|
+
engine in place of the original. The Eleventy server-side filter is
|
|
315
|
+
untouched.
|
|
316
|
+
|
|
317
|
+
> Aside: if you have an existing filter that returns the current page's URL
|
|
318
|
+
> via `this.page.url`, your template can use the `page` global directly
|
|
319
|
+
> instead — `{{ page.url }}` works server-side and in the editor. Avoid
|
|
320
|
+
> writing a browser-side override that reads `location.pathname`; inside
|
|
321
|
+
> CloudCannon's Visual Editor that returns CC's editor-shell URL, not the site URL.
|
|
322
|
+
|
|
323
|
+
### Overriding a built-in
|
|
324
|
+
|
|
325
|
+
If you replace a built-in name in your Eleventy config —
|
|
326
|
+
`eleventyConfig.addFilter("url", myCustomUrl)` — your replacement applies
|
|
327
|
+
server-side, but the live-editing bundle still uses our handwritten port
|
|
328
|
+
(the auto-mirror skips built-in names). To make the override apply in the
|
|
329
|
+
bundle too, register a second time via `pluginOptions.liquid.filters`:
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
// eleventy.config.mjs
|
|
333
|
+
eleventyConfig.addFilter("url", myCustomUrl); // server-side
|
|
334
|
+
|
|
335
|
+
eleventyConfig.addPlugin(editableRegions, {
|
|
336
|
+
liquid: {
|
|
337
|
+
filters: {
|
|
338
|
+
url: "./live-editing-overrides/url.mjs", // live editing
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
This only applies to built-in names: the auto-mirror skips them to protect our
|
|
345
|
+
browser ports, so the second registration is what unlocks your override.
|
|
346
|
+
|
|
347
|
+
## Shortcodes and paired shortcodes
|
|
348
|
+
|
|
349
|
+
Same built-ins / auto-mirrored / overrides model as filters: anything
|
|
350
|
+
registered via `addShortcode` / `addAsyncShortcode` / `addLiquidShortcode`
|
|
351
|
+
(and the paired equivalents) is mirrored with closures intact, and a
|
|
352
|
+
non-portable one throws at render time, prompting an override under
|
|
353
|
+
`pluginOptions.liquid.shortcodes` / `pluginOptions.liquid.pairedShortcodes`.
|
|
354
|
+
|
|
355
|
+
The only built-in shortcode is `renderFile`, one of the three RenderPlugin
|
|
356
|
+
shims — `renderContent` is a [filter](#filters) and `renderTemplate` is a
|
|
357
|
+
[tag](#built-in-tags). See [RenderPlugin shims](#renderplugin-shims).
|
|
358
|
+
|
|
359
|
+
### Adding a custom shortcode
|
|
360
|
+
|
|
361
|
+
Like filters: register with Eleventy as normal and the auto-mirror handles
|
|
362
|
+
it.
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
// eleventy.config.mjs
|
|
366
|
+
eleventyConfig.addShortcode("year", () => new Date().getFullYear());
|
|
367
|
+
// → `{% year %}` works in live editing automatically
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
For paired shortcodes, use `addPairedShortcode`:
|
|
371
|
+
|
|
372
|
+
```js
|
|
373
|
+
eleventyConfig.addPairedShortcode("highlight", (content, color = "yellow") =>
|
|
374
|
+
`<mark style="background:${color}">${content}</mark>`,
|
|
375
|
+
);
|
|
376
|
+
// → {% highlight "lime" %}note{% endhighlight %}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
If a shortcode reads from Eleventy's runtime state (`this.page`,
|
|
380
|
+
`this.ctx`, etc.), provide a browser override via
|
|
381
|
+
`pluginOptions.liquid.shortcodes` or `pluginOptions.liquid.pairedShortcodes`,
|
|
382
|
+
same shape as the filter override above.
|
|
383
|
+
|
|
384
|
+
## Tags
|
|
385
|
+
|
|
386
|
+
Same auto-mirror + override model as filters and shortcodes. A tag
|
|
387
|
+
registered with `addLiquidTag` is replayed from the bundled config, so the
|
|
388
|
+
factory and everything it closes over — including LiquidJS internals like
|
|
389
|
+
`Tokenizer` / `evalToken` / `toPromise` — survive into the browser with no
|
|
390
|
+
extra work:
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
// eleventy.config.mjs
|
|
394
|
+
eleventyConfig.addLiquidTag("echo", echoTagFactory);
|
|
395
|
+
// → `{% echo %}` works in live editing automatically
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The factory is the value `addLiquidTag` expects:
|
|
399
|
+
`(liquidEngine) => ({ parse, render })`.
|
|
400
|
+
|
|
401
|
+
Override only a tag that can't run in the browser as written, via
|
|
402
|
+
`pluginOptions.liquid.tags` (tag name → module path, default-exporting the
|
|
403
|
+
same factory shape). The override's name is skipped by the auto-mirror so
|
|
404
|
+
the override is the sole registration:
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
liquid: {
|
|
408
|
+
tags: {
|
|
409
|
+
myTag: "./src/live-editing/my-tag.mjs",
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
If a template references an unregistered tag, `enhanceLiquidError` rewrites
|
|
415
|
+
LiquidJS's "tag X not found" into an actionable message pointing the user at
|
|
416
|
+
`pluginOptions.liquid.tags`.
|
|
417
|
+
|
|
418
|
+
### Built-in tags
|
|
419
|
+
|
|
420
|
+
The runtime registers a few tags of its own at engine creation time. Users
|
|
421
|
+
don't have to do anything to get these.
|
|
422
|
+
|
|
423
|
+
**`includeWith`** — spreads an object into an include the way Astro's
|
|
424
|
+
`{...props}` does. Wired up in both the Eleventy build (so server-rendered
|
|
425
|
+
output works) and `createSharedLiquidEngine` (so live editing matches).
|
|
426
|
+
Pass a variable that references the object you want to spread — front
|
|
427
|
+
matter, an `assign`-ed name, or a global like `page`:
|
|
428
|
+
|
|
429
|
+
```liquid
|
|
430
|
+
{% includeWith "components/card", cardProps %}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
The second argument must be a variable reference; inline object literals
|
|
434
|
+
(`{ key: value }`) aren't standard Liquid syntax and aren't supported.
|
|
435
|
+
|
|
436
|
+
**`renderTemplate`** — RenderPlugin shim. A paired tag that compiles the
|
|
437
|
+
body as a Liquid template and renders it against the supplied data. Same
|
|
438
|
+
constraint as `includeWith`: the data argument must be a variable
|
|
439
|
+
reference.
|
|
440
|
+
|
|
441
|
+
```liquid
|
|
442
|
+
{% renderTemplate "liquid", templateData %}
|
|
443
|
+
Hello {{ name }}
|
|
444
|
+
{% endrenderTemplate %}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Only `"liquid"` and `"html"` engines are supported in the browser (other
|
|
448
|
+
engines warn once and return the body unchanged). See `eleventy/browser/liquid-render.mjs`.
|
|
449
|
+
|
|
450
|
+
`renderFile` and `renderContent` are also part of the RenderPlugin shim —
|
|
451
|
+
documented in the next section since they're shortcode/filter rather than
|
|
452
|
+
tag-shaped.
|
|
453
|
+
|
|
454
|
+
### RenderPlugin shims
|
|
455
|
+
|
|
456
|
+
Eleventy's `RenderPlugin` registers three template-side helpers. We
|
|
457
|
+
reimplement all three in the browser, scoped to the engines we actually
|
|
458
|
+
run there.
|
|
459
|
+
|
|
460
|
+
> **Server-side note:** 11ty 3.x ships `RenderPlugin` but doesn't auto-load
|
|
461
|
+
> it. If you want the helpers to work in your Eleventy build (in addition
|
|
462
|
+
> to live editing), explicitly add it in `eleventy.config.mjs`:
|
|
463
|
+
>
|
|
464
|
+
> ```js
|
|
465
|
+
> import { EleventyRenderPlugin } from "@11ty/eleventy";
|
|
466
|
+
> eleventyConfig.addPlugin(EleventyRenderPlugin);
|
|
467
|
+
> ```
|
|
468
|
+
>
|
|
469
|
+
> Our browser-side shims work either way.
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
| Helper | Shape | Usage |
|
|
473
|
+
| --- | --- | --- |
|
|
474
|
+
| `renderTemplate` | paired Liquid tag | `{% renderTemplate "liquid", data %}…{% endrenderTemplate %}` |
|
|
475
|
+
| `renderFile` | async shortcode | `{% renderFile "path/to/file.liquid", data %}` |
|
|
476
|
+
| `renderContent` | async filter | `{{ rawString \| renderContent: "liquid", data }}` |
|
|
477
|
+
|
|
478
|
+
All three share the same behaviour: `"liquid"` (or unspecified) → real
|
|
479
|
+
parse-and-render through the shared engine; `"html"` → identity
|
|
480
|
+
passthrough; any other engine → warn-once and return the body unchanged.
|
|
481
|
+
`renderFile` fetches the target via the CloudCannon Visual Editor API
|
|
482
|
+
(`CloudCannon.file(path).content.get()`), which returns the file body with
|
|
483
|
+
front matter stripped — matching how Eleventy feeds a template body to its
|
|
484
|
+
engine. Any file the editor can see is reachable, not just files inside a
|
|
485
|
+
configured `componentDir`. (`{% include %}` is the separate path: it goes
|
|
486
|
+
through LiquidJS's filesystem, which is the build-time `cc_liquid_files` map.)
|
|
487
|
+
|
|
488
|
+
## Component resolution
|
|
489
|
+
|
|
490
|
+
Components are accessed as `window.cc_components[name](props)`. There are
|
|
491
|
+
two resolution paths; the proxy is the primary one and the explicit map is
|
|
492
|
+
the override.
|
|
493
|
+
|
|
494
|
+
1. **Include-resolution proxy** (the primary path). For any unrecognised name,
|
|
495
|
+
the proxy returns a renderer that runs `{% include "<name>" %}` against the
|
|
496
|
+
shared engine, which resolves the file via the configured component
|
|
497
|
+
directories and `extensions`. This is how every auto-discovered component
|
|
498
|
+
becomes reachable with no explicit registration.
|
|
499
|
+
2. **Explicit registrations** via `pluginOptions.liquid.components` — a map of
|
|
500
|
+
`name -> module path`. The module's default export is treated as Liquid
|
|
501
|
+
template source for that name, taking precedence over include resolution.
|
|
502
|
+
Use this to substitute a different template for a specific name.
|
|
503
|
+
|
|
504
|
+
Both paths render to a detached `<div>` and return it as an `HTMLElement`.
|
|
505
|
+
|
|
506
|
+
## Virtual filesystem
|
|
507
|
+
|
|
508
|
+
Two data sources back the runtime:
|
|
509
|
+
|
|
510
|
+
- **`window.cc_liquid_files`** — a build-time snapshot of your templates,
|
|
511
|
+
serving `{% include %}` resolution synchronously.
|
|
512
|
+
- **CloudCannon Visual Editor API** (`CloudCannon.currentFile()`,
|
|
513
|
+
`CloudCannon.file(path)`, `CloudCannon.collection(key)`) — a live view of the
|
|
514
|
+
editor's file tree, backing the `page` / `collections` globals and
|
|
515
|
+
`renderFile`.
|
|
516
|
+
|
|
517
|
+
When in doubt, prefer the API: it sees everything the editor sees and stays
|
|
518
|
+
correct as the user edits.
|
|
519
|
+
|
|
520
|
+
## Error enhancement
|
|
521
|
+
|
|
522
|
+
Three categories of LiquidJS error are rewritten into actionable messages with
|
|
523
|
+
the component/template name and a concrete next step:
|
|
524
|
+
|
|
525
|
+
- Unknown filter → "register it in the `filters` option"
|
|
526
|
+
- Missing template (`ENOENT …`) → "check the file is in your component dirs"
|
|
527
|
+
- Unknown tag → "register it in `tags`, `shortcodes`, or `pairedShortcodes`"
|
|
528
|
+
|
|
529
|
+
Anything else falls through with the component name prefixed.
|
|
530
|
+
|
|
531
|
+
## Limitations and fallbacks
|
|
532
|
+
|
|
533
|
+
The runtime can't reproduce everything Eleventy does at build time. This
|
|
534
|
+
section catalogues the gaps and the patterns for working around them.
|
|
535
|
+
|
|
536
|
+
### Things that don't work in live editing
|
|
537
|
+
|
|
538
|
+
| Area | What happens | Fallback |
|
|
539
|
+
| --- | --- | --- |
|
|
540
|
+
| `htmlBaseUrl`, `serverlessUrl` filters | Registered as warn-once pass-throughs; return their input unchanged. `htmlBaseUrl` depends on the configured `pathPrefix` (we don't expose it yet); `serverlessUrl` is a build-time concept with no editor equivalent. | Override via `pluginOptions.liquid.filters` if you have a browser-safe equivalent. Otherwise wrap the template path in `{% if ENV_CLIENT %}` and skip it. |
|
|
541
|
+
| `inputPathToUrl` filter when the source file wasn't in the last build | Falls back to warn-once and returns the input path unchanged. The build-time page map is what makes this filter work; for files added since the last build there's no URL to look up. | Re-build to pick up new pages. |
|
|
542
|
+
| `renderTemplate` / `renderFile` / `renderContent` with a non-Liquid engine arg (e.g. `"njk"`, `"md"`) | Warn-once and return the body unchanged. We only ship LiquidJS in the bundle. | Switch the template to Liquid, or guard the call with `{% if ENV_CLIENT %}` so it only runs at build time. |
|
|
543
|
+
| Mirrored filters/shortcodes that touch `this.ctx`, `process`, `require`, `__dirname`, or a closed-over Node import | Auto-mirror ships them verbatim; they throw at render time in the browser. The thrown error is wrapped by `enhanceLiquidError` with the filter/shortcode name. | Add a `pluginOptions.liquid.filters` (or `.shortcodes` / `.pairedShortcodes`) override pointing at a browser-safe replacement. |
|
|
544
|
+
| Helpers from auto-loaded 11ty plugins used **inside a component** (e.g. `getBundle` / `getBundleFileUrl` / `renderTransforms` from `@11ty/eleventy-plugin-bundle`) | 11ty 3.x auto-loads several plugins that register universal helpers; the auto-mirror ships them verbatim and they'll throw if invoked from a template the editor re-renders. Layouts and pages aren't affected — the live runtime only renders components. | If you reference one of these in an editable component, add a browser-safe override via `pluginOptions.liquid.shortcodes` / `.filters`. Most users won't hit this because bundle helpers typically live in layouts. |
|
|
545
|
+
| User overrides of a **built-in** filter name via `eleventyConfig.addFilter` | The auto-mirror skips built-in names, so the override doesn't reach the bundle — live editing keeps using our handwritten port. | Also register the override in `pluginOptions.liquid.filters`. See "Overriding a built-in". |
|
|
546
|
+
| Custom Liquid tags | Not auto-mirrored. Templates referencing an unregistered custom tag will fail with an enhanced "tag X not found" error. | Register every tag you want available via `pluginOptions.liquid.tags`. |
|
|
547
|
+
| `page.templateSyntax`, `page.lang` | `undefined`. | If you need them, read from front matter / `_data/` instead, or skip the branch via `ENV_CLIENT`. |
|
|
548
|
+
| `page.date` from file mtime / git history | `undefined` if not in front matter. | Set `date:` in front matter. |
|
|
549
|
+
| `eleventy.env.config`, `eleventy.env.root` | Deliberately omitted (absolute filesystem paths). | Don't reference these from a component. |
|
|
550
|
+
| `eleventy.env.runMode`, `eleventy.env.source` | Hardcoded to `"serve"` / `"cli"`. | If you need a "we're in the editor" branch, use `ENV_CLIENT` instead. |
|
|
551
|
+
| `pagination`, `eleventy.serverless` | Not exposed. | Pagination is a build-time-only data cascade; serverless was removed upstream. |
|
|
552
|
+
| Layout files | Not rendered by the live runtime; the page's HTML stays as Eleventy built it. | Layout-dependent logic should live in the component, not the layout, if you want it editable. |
|
|
553
|
+
|
|
554
|
+
### Patterns
|
|
555
|
+
|
|
556
|
+
**Branching on "are we in the editor?".** Use the `ENV_CLIENT` global, which
|
|
557
|
+
is `true` in the live-editing bundle and `false`/undefined during the
|
|
558
|
+
Eleventy build:
|
|
559
|
+
|
|
560
|
+
```liquid
|
|
561
|
+
{% if ENV_CLIENT %}
|
|
562
|
+
<p>Editing — placeholder shown.</p>
|
|
563
|
+
{% else %}
|
|
564
|
+
{{ collections.posts | someBuildOnlyFilter }}
|
|
565
|
+
{% endif %}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
This is the right escape hatch for build-only logic that you don't want
|
|
569
|
+
running in the editor at all.
|
|
570
|
+
|
|
571
|
+
**Overriding a single filter / shortcode / tag with a browser version.**
|
|
572
|
+
Point the relevant `pluginOptions.liquid.{filters,shortcodes,pairedShortcodes,tags}`
|
|
573
|
+
entry at a module that default-exports a browser-safe replacement. The
|
|
574
|
+
override only applies to live editing — your Eleventy server-side
|
|
575
|
+
registration keeps working unchanged.
|
|
576
|
+
|
|
577
|
+
**Replacing an entire component for live editing.** If a single component
|
|
578
|
+
has too many incompatibilities to override piecemeal, register a
|
|
579
|
+
component-specific renderer via `pluginOptions.liquid.components`:
|
|
580
|
+
the module's default export is treated as Liquid template source for that
|
|
581
|
+
component name, fully replacing what's on disk.
|
|
582
|
+
|
|
583
|
+
**When you need data the shims don't have.** Pull from `_data/` (which
|
|
584
|
+
becomes the front matter / data cascade and is readable via the
|
|
585
|
+
`collections` proxy), or from the CloudCannon JS API directly in a custom
|
|
586
|
+
tag or filter override. The Visual Editor exposes `currentFile()`,
|
|
587
|
+
`collection(key)`, `dataset(key)`, and `file(path)` — see the existing
|
|
588
|
+
`page` proxy in `globals.mjs` for a reference implementation.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrites a LiquidJS error into an actionable message for the error card.
|
|
3
|
+
*
|
|
4
|
+
* @param {unknown} err
|
|
5
|
+
* @param {string} componentName
|
|
6
|
+
* @returns {Error}
|
|
7
|
+
*/
|
|
8
|
+
export function enhanceLiquidError(err, componentName) {
|
|
9
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10
|
+
|
|
11
|
+
const unknownFilter = message.match(/undefined filter[:.]?\s*(\S+)/i);
|
|
12
|
+
if (unknownFilter) {
|
|
13
|
+
const filterName = unknownFilter[1];
|
|
14
|
+
return new Error(
|
|
15
|
+
`Unknown filter "${filterName}" while rendering "${componentName}". ` +
|
|
16
|
+
`Please check your config and make sure you have registered "${filterName}" in the filters option.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const missingTemplate = message.match(/ENOENT.*?"([^"]+)"/);
|
|
21
|
+
if (missingTemplate) {
|
|
22
|
+
const filePath = missingTemplate[1];
|
|
23
|
+
return new Error(
|
|
24
|
+
`Failed to find included template "${filePath}" while rendering "${componentName}". ` +
|
|
25
|
+
`Please check that the file exists and is within your configured component directories.`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const missingTag = message.match(/tag "?(\S+?)"? not found/i);
|
|
30
|
+
if (missingTag) {
|
|
31
|
+
const tagName = missingTag[1];
|
|
32
|
+
return new Error(
|
|
33
|
+
`Unknown tag "${tagName}" while rendering "${componentName}". ` +
|
|
34
|
+
`Please check your config and make sure you have registered "${tagName}" in the tags, shortcodes, or pairedShortcodes option.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new Error(`Error rendering "${componentName}": ${message}`);
|
|
39
|
+
}
|