astro/bookshop.md
# Bookshop in Astro
Astro's delta from [../bookshop.md](../bookshop.md), which owns the concepts. Read that first.
## Where things live
| Piece | Location |
| ------------ | ---------------------------------------------------------------------------------------------- |
| Library root | `src/` — the parent of `src/bookshop/` |
| Config | `src/bookshop/bookshop.config.cjs`, naming `@bookshop/astro-engine` |
| Components | `src/components/<path>/<name>/<name>.{astro,jsx,tsx}` with `<name>.bookshop.yml` beside it |
| Dispatchers | `src/shared/astro/` — site code, not Bookshop's (see [§ Dispatchers](#dispatchers)) |
| Integration | `bookshop()` from `@bookshop/astro-bookshop` in `astro.config.mjs` `integrations` |
| Packages | `@bookshop/astro-bookshop`, `@bookshop/astro-engine`, `@bookshop/generate` — one exact version |
## What the integration does
`bookshop()` is more than a live-editing switch. While it is installed it:
- **Rewrites every component** under `src/components`, `src/layouts` and `src/pages` through a Vite plugin, adding binding data to props and output. Built HTML carries attributes and comments that aren't in source.
- **Defines `ENV_BOOKSHOP_LIVE`** — `false` in the build, true in the live engine.
- **Auto-imports** every `src/components/**` component into Markdown and MDX under its PascalCase name, so MDX can use components it never imports.
## `bookshop:live`
A `bookshop:live` attribute on a component call in an `.astro` file makes that call live-editable:
```astro
<Page bookshop:live contentBlocks={props.content_blocks} />
<HeroBlock bookshop:live heroBlock={props.hero_block} />
```
The component must live under `src/components/` or `src/shared/astro/`. `bookshop:binding={false}` on a call turns off its click-to-edit binding.
## Dispatchers
Bookshop has no Astro dispatcher of its own. Bookshop Astro sites ship one in `src/shared/astro/`, usually `page.{astro,jsx}`, that globs the components and selects on `_bookshop_name`:
```astro
---
const components = {};
const imports = import.meta.glob("../../components/**/*.{jsx,astro}", { eager: true });
Object.entries(imports).forEach(([path, obj]) => {
const parts = path.replace("../../components/", "").split(".")[0].split("/");
if (parts.length > 1 && parts[parts.length - 1] === parts[parts.length - 2]) parts.pop();
components[parts.join("/")] = obj.default;
});
---
{contentBlocks.map((block, i) => {
const Component = components[block._bookshop_name];
return <Component {...block} key={i} />;
})}
```
The key is Bookshop's naming rule — the path without the extension, with a repeated last segment collapsed. A single-component slot (`hero_block`) often has its own dispatcher of the same shape. Components that render nested components (buttons) often repeat the lookup inside, reading `_bookshop_name` in component code.
## Live editing in Astro
The live engine runs Astro's renderer in the browser, with the library only.
| Limit | Work around it |
| ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
| Only `astro:content`, `astro:assets`, `astro:i18n`, `astro:actions`, `astro:middleware`, `astro:env` and `astro:transitions` resolve | Branch on `ENV_BOOKSHOP_LIVE` for anything else |
| Collections come from CloudCannon's data; an entry's body and `render()` are unavailable | Branch on `ENV_BOOKSHOP_LIVE` |
| Config is read from `astro.config.mjs` only — any other filename is ignored | Keep the config in `astro.config.mjs` |
| React is the only framework renderer | Vue, Svelte and Solid components can't live-render |
| Only `PUBLIC_*` environment variables | — |
## Adding a component
`@bookshop/init` has no Astro template. Create `src/components/<path>/<name>/<name>.astro` and `<name>.bookshop.yml` by hand, or with the site's own script if it ships one (`npm run new-component`). Then follow [../maintaining.md § Adding a component](../maintaining.md#adding-a-component). The glob dispatcher picks the component up without a change.
## Versions
Astro 5.8.1 and later need Bookshop 3.16.2 or later for live editing.
astro/migrating-to-editable-regions.md
# Migrating to editable regions (Astro)
Astro's delta from [../migrating-to-editable-regions.md](../migrating-to-editable-regions.md), which owns the step order, the structure mapping, the `_bookshop_name` rename and the removal checklist. Read that first.
## What each step means in Astro
| Step in the base file | Astro work |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Baseline | Nothing extra — no version upgrade is needed on Astro 5 |
| Structures | The library root to copy into the scratch directory is `src/`. Split co-located structure files work well: `<name>.cloudcannon.structure-value.yml` beside each component, named after the component's last path segment. Give each `_structures.<key>` its own `values_from_glob`, scoped to its components' directories — one `**` glob over `src/components/` would put buttons in the page builder — see [structures.md](../../cloudcannon-configuration/structures.md) |
| Rename, resolve and dispatch — one step | Nothing moves. Change the lookups only — the region wrappers and `data-*` attributes shown below go in at step 5, after the census. Build a [component map](#keep-bookshops-names-in-a-component-map) with Bookshop's naming rule, and [replace each dispatcher](#replace-the-dispatchers) |
| Remove Bookshop | [§ Removing the integration](#removing-the-integration), with the base checklist |
| Add regions | Run the setup in [cloudcannon-visual-editing/astro/overview.md](../../cloudcannon-visual-editing/astro/overview.md), then [§ Registering the components](#registering-the-components) and [§ Props the dispatcher computed](#props-the-dispatcher-computed) |
## Keep Bookshop's names in a component map
**MUST:** key the map with the dispatcher's own rule, so every `_type` value in content still resolves.
**Why:** content stores `heroes/hero`, not a new key. Reusing the rule means no content value changes.
```ts
// src/cloudcannon/componentMap.ts — shared by the build and the editor
function byBookshopName(modules: Record<string, { default: any }>) {
const map: Record<string, any> = {};
for (const [path, module] of Object.entries(modules)) {
const parts = path
.replace("../components/", "")
.replace(/\.\w+$/, "")
.split("/");
const n = parts.length;
if (n < 2 || parts[n - 1] !== parts[n - 2]) continue; // helpers, not components
parts.pop();
map[parts.join("/")] = module.default;
}
return map;
}
export const astroComponents = byBookshopName(
import.meta.glob("../components/**/*.astro", { eager: true }),
);
export const reactComponents = byBookshopName(
import.meta.glob("../components/**/*.jsx", { eager: true }),
);
export const componentMap = { ...astroComponents, ...reactComponents };
```
Glob only the extensions the old dispatcher globbed. Keep the maps apart for registration — see [§ Registering the components](#registering-the-components).
The eager glob imports every file it matches, and each imported component's CSS then reaches every page that uses the map. Narrow the glob pattern — not the loop — if that weight matters.
## Replace the dispatchers
| Bookshop | Editable regions |
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<Page bookshop:live contentBlocks={…} />` + `src/shared/astro/page.*` | A loop component with the array regions — [§ The page-builder loop](#the-page-builder-loop) |
| `<HeroBlock bookshop:live heroBlock={…} />` — one slot, several structures | A registered dispatcher — [§ One slot, several structures](#one-slot-several-structures) |
| `<PostHero bookshop:live {...post_hero} />` — one slot, one component | `<editable-component data-component="blog/post-hero" data-prop="post_hero">` around the call |
| `button._bookshop_name` lookups inside components | The same lookup on `_type`. Wrap an array of them in `data-editable="array" data-component-key="_type"` with an `<editable-array-item data-component={button._type}>` per item; wrap a single one in `<editable-component data-component={button._type} data-prop="button">` |
| `ENV_BOOKSHOP_LIVE` | `ENV_CLIENT` — a bare global, not `import.meta.env.ENV_CLIENT` |
### The page-builder loop
**MUST:** put the loop in its own component (`src/layouts/ContentBlocks.astro`), not in the layout.
**Why:** a Markdown page that uses the layout through `layout:` front matter — `src/pages/404.md` is common — gets its stylesheet emitted before `<html>` when the layout itself contains the loop's elements. Bookshop hid this, because its dispatcher was a component.
```astro
---
// src/layouts/ContentBlocks.astro
import { componentMap } from "../cloudcannon/componentMap";
const blocks = Astro.props.blocks ?? [];
---
<div data-editable="array" data-prop="content_blocks" data-component-key="_type">
{blocks.map((block) => {
const Block = componentMap[block._type];
if (!Block) throw new Error(`Unknown component: ${block._type}`);
return (
<div data-editable="array-item" data-component={block._type}>
<Block {...block} />
</div>
);
})}
</div>
```
- **Read** props directly (`Astro.props.blocks ?? []`), not by destructuring with a default. While `bookshop()` is still installed at step 3, its rewrite breaks destructured props and the page drops out of the build.
- **Pass** only the block's data. Anything the old dispatcher computed (`key`, `content_blocks_length`) moves into the components → [§ Props the dispatcher computed](#props-the-dispatcher-computed).
- **Use** the element the old dispatcher's parent expected — the layout usually owns `<main>` already.
- **Throw** on an unknown name: a guarded lookup hides a missed rename.
### One slot, several structures
A single object that can be any of several components (`hero_block` from `_structures.hero_blocks`) can't use `data-component={hero_block._type}` — that is fixed at build, so switching the structure in the editor renders the new data through the old component and shows an error card.
**MUST:** register a thin dispatcher and name it instead.
```astro
---
// src/components/BlockRenderer.astro
import { componentMap } from "../cloudcannon/componentMap";
const Component = componentMap[Astro.props._type];
---
{Component && <Component {...Astro.props} />}
```
```astro
<editable-component data-component="block-renderer" data-prop="hero_block">
<BlockRenderer {...heroBlock} />
</editable-component>
```
## Registering the components
Loop over the maps in `registerComponents.ts`, each with its own function → [visual editing § BlockRenderer architecture](../../cloudcannon-visual-editing/astro/visual-editing-reference.md#blockrenderer-architecture):
| Component type | Register with |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `.astro` | `registerAstroComponent(name, component)` from `@cloudcannon/editable-regions/astro` |
| `.astro` that renders React, Vue or Svelte | The same, plus that framework's `import "@cloudcannon/editable-regions/astro-<framework>-renderer"` first |
| `.jsx` / `.tsx` | `registerReactComponent(name, component)` from `@cloudcannon/editable-regions/react` — keep the blocks as React |
```ts
import { registerAstroComponent } from "@cloudcannon/editable-regions/astro";
import { registerReactComponent } from "@cloudcannon/editable-regions/react";
import { astroComponents, reactComponents } from "./componentMap";
for (const [name, c] of Object.entries(astroComponents)) registerAstroComponent(name, c);
for (const [name, c] of Object.entries(reactComponents)) registerReactComponent(name, c);
```
Register the `block-renderer` dispatcher too, if the site has one. A React block renders from the `.astro` loop with no `client:` directive.
A React block's region host sits outside the component, as the array-item `<div>` in the loop does → [cloudcannon-visual-editing § React, Vue and Svelte components](../../cloudcannon-visual-editing/visual-editing-reference.md#react-vue-and-svelte-components).
## Props the dispatcher computed
Bookshop dispatchers often pass props that aren't in the block's data, such as the item's index or the array's length. The editor re-renders a block from its data alone, so these arrive `undefined` there. Remove the dependency from the component:
| The block used it for | Replace with |
| ------------------------------------------------------------------ | ------------------------------------------------------------------ |
| Spacing or styling by position (first, last, alternating) | `:first-child`, `:last-child`, `:nth-child(even)` in the block CSS |
| A value editors should control (an anchor id, a background choice) | A field in the block's data, with an input |
**Only** when the position itself changes the markup (an `<h1>` on the first block only, say), pass it both ways: `index={i}` on the component for the build, and `data-prop-index="@index"` on the array item for the editor. `@length` gives the item count the same way. Name these props in lowercase or snake_case, because `data-prop-*` names are lowercased. Blocks added in the editor don't get them until the next build.
## Removing the integration
Remove `bookshop()` from `astro.config.mjs` at step 4, with the packages. Then:
- [ ] Replace every `ENV_BOOKSHOP_LIVE` with `ENV_CLIENT` — `grep -rn ENV_BOOKSHOP_LIVE src`.
- [ ] Replace the integration's auto-import without adding `import` lines to MDX content — editors see them in the Content Editor. Register the components MDX content uses (`rg '<[A-Z]' -g '*.mdx' src/content`) with `astro-auto-import` before `mdx()`, or pass a shared `components` map at every render site, and delete any `import` lines already in content → [MDX setup pipeline](../../cloudcannon-snippets/astro/overview.md#mdx-setup-pipeline-must-complete-all-four).
- [ ] Remove `src/bookshop/` and `src/shared/astro/`, and any path alias pointing at `src/shared` in `tsconfig.json`. Imports rooted at `baseUrl` (`src/shared/…`) go with them; keep `baseUrl` itself if other imports use it.
- [ ] Check the site's Node version meets `@cloudcannon/editable-regions`' `engines.node` — Bookshop-era sites often pin Node 18. On a hosted site, change it in the site's build settings (or with [cloudcannon-cli](../../cloudcannon-cli/SKILL.md)); `.cloudcannon/initial-site-settings.json` only applies when a site is created.
## Parity on Astro
- **Diff** with Bookshop's markup ignored: `data-cms-bind` attributes and `<!--databinding:…-->` comments exist only in the Bookshop build output.
- **Expect** component CSS to move: Bookshop inlined it on pages using `bookshop:live`; after the migration the same rules are linked, and pages gaining some is expected.
- **Expect** MDX pages to lose the CSS of components the integration auto-imported but the page never renders. Check each lost rule matches nothing on that page; any other lost rule is a regression.
astro/overview.md
# Bookshop (Astro) — entry point
Astro-specific Bookshop guidance. The cross-SSG rules live one level up; the files here carry only what differs for Astro.
**Two differences shape everything else:**
- **The site owns the dispatcher.** Bookshop resolves nothing itself on Astro — a glob in `src/shared/astro/` maps `_bookshop_name` to components, and `bookshop:live` makes a call live-editable. Migrating keeps that naming rule, so components never move.
- **The integration rewrites every component while it's installed.** Built output differs from source until `bookshop()` is removed, which is why removal comes right after the dispatcher swap.
## Reading order
| Order | File | Read when |
| ----- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| 1 | [../bookshop.md](../bookshop.md) | Always — the pieces of a Bookshop site |
| 2 | [bookshop.md](bookshop.md) | Always — where they live in Astro, `bookshop:live`, the dispatchers |
| 3 | [../maintaining.md](../maintaining.md) | The site stays on Bookshop |
| 3 | [../migrating-to-editable-regions.md](../migrating-to-editable-regions.md) | The site is migrating — the step order and cross-SSG mapping |
| 4 | [migrating-to-editable-regions.md](migrating-to-editable-regions.md) | The site is migrating — the component map, dispatchers, registration |
| 5 | [troubleshooting.md](troubleshooting.md) | An Astro symptom, after checking the generic table |
astro/troubleshooting.md
# Troubleshooting (Astro)
Astro rows only. Check [../troubleshooting.md](../troubleshooting.md) first.
## On a Bookshop site
| Symptom | Cause | Fix |
| ---------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Editor console: "The 'astro:x' module is not supported inside Bookshop components" | The live engine can't provide that virtual module | Branch on `ENV_BOOKSHOP_LIVE` — [bookshop.md § Live editing in Astro](bookshop.md#live-editing-in-astro) |
| Live editing stopped working after an Astro upgrade | Astro 5.8.1 and later need Bookshop 3.16.2 or later | Upgrade Bookshop |
| Build log: "No live editing connected…" on an Astro site | No call has `bookshop:live` | Add it to the component calls in the layout |
## Partway through a migration
| Symptom | Cause | Fix |
| ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Build error: "Element type is invalid…" or "Unable to render Component because it is undefined!" | `_bookshop_name` renamed while the old dispatcher still reads it | Replace the dispatcher in the same step |
| A hero or single block silently disappears | Its dispatcher guards the lookup, so a missed rename renders nothing | Rename in component code too; throw on an unknown name in the new dispatcher |
| A Markdown page's `<link rel="stylesheet">` lands before `<html>` | The page's `layout:` contains the page-builder loop directly | Move the loop into its own component — [migrating-to-editable-regions.md § The page-builder loop](migrating-to-editable-regions.md#the-page-builder-loop) |
| Build error after uninstalling Bookshop: `Preprocessor dependency "sass-embedded" not found` | `sass` was only installed through `@bookshop/astro-engine` | Install `sass` as a devDependency at the baseline's version |
| Switching a single slot's structure in the editor shows an error card | `data-component` names the component chosen at build | [migrating-to-editable-regions.md § One slot, several structures](migrating-to-editable-regions.md#one-slot-several-structures) |
| A block renders differently in the editor than on the built page (spacing, ids) | It uses a prop the dispatcher computed | Remove the dependency → [migrating-to-editable-regions.md § Props the dispatcher computed](migrating-to-editable-regions.md#props-the-dispatcher-computed) |
| A camelCase prop fed by `data-prop-*` is undefined | `data-prop-*` names are lowercased | Rename the prop to snake_case |
| An MDX page fails to build after removing the integration | It used a component the integration auto-imported | Add it to `AutoImport` or the shared `components` map, not an `import` in the MDX file → [MDX setup pipeline](../../cloudcannon-snippets/astro/overview.md#mdx-setup-pipeline-must-complete-all-four) |
bookshop.md
# How Bookshop works
The concepts every Bookshop site shares. Template syntax and file locations differ per SSG — [Astro](astro/bookshop.md), [Hugo](hugo/bookshop.md), [Eleventy](eleventy/bookshop.md), [Next.js](nextjs/bookshop.md).
## The pieces
| Piece | What it is |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Component library | A directory of components, each a template plus a `*.bookshop.yml` (and optionally a `.scss`). Its root is the parent of `bookshop/bookshop.config.cjs` |
| `bookshop.config.cjs` | Names the live-editing engine (`engines: { "@bookshop/hugo-engine": {} }`) and optional `ignoreFilePatterns` |
| The SSG plugin | Lets the site's templates render library components — an Astro integration, a Hugo module, an Eleventy plugin, a Jekyll gem |
| `@bookshop/generate` | Runs in `.cloudcannon/postbuild`, after the site builds. Writes `_structures` into the build output and injects the live-editing script |
| The live engine | A browser build of the SSG's renderer that re-renders components in the Visual Editor as the editor types |
| `_bookshop_name` | The key in front matter that names which component a block is |
## Component names
A component's name is its path under `components/`, without the extension. A repeated last segment collapses: `components/home/hero/hero.hugo.html` is `home/hero`. That name is the `_bookshop_name` value in content and the component path in every call.
## `*.bookshop.yml`
One per component. The format can also be `.yaml`, `.toml`, `.json`, `.js` or `.cjs`.
```yaml
spec:
structures: [content_blocks]
label: Counter
description: Counter section
icon: functions
tags: [stats]
blueprint:
title: Scale your
numbers:
- number: 200
text: Venture capital raised
button: bookshop:button
preview:
title: Scale your business
_inputs:
numbers:
comment: Up to four
```
| Key | Meaning |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `spec.structures` | Which `_structures.<key>` lists this component joins. An array input pointing at `_structures.content_blocks` can then add it |
| `spec.label` etc. | Picker metadata. Every other `spec.*` key passes through onto the structure value too |
| `blueprint` | The component's props and their values when it is added. Should list every prop the template reads |
| `preview` | Values merged over the blueprint for the component browser only. CloudCannon never sees it |
| `_inputs`, `_*` | Passed through to the structure value, and cascaded into the structures generated for nested arrays |
| `bookshop:<name>` | In a blueprint: a nested component slot. `[bookshop:<name>]` is an array of them; `bookshop:structure:<key>` a slot for any component in that structure |
A `<name>.preview.<ext>` or `<name>.icon.<ext>` image beside the component becomes the picker thumbnail.
## What `@bookshop/generate` does
**MUST:** treat everything it produces as absent from the repo. It runs after the build, on CloudCannon, and writes into the output.
**Why:** the committed `cloudcannon.config.yml` often has `_structures` keys with no `values` — the values only ever existed in the built site.
- **Structures.** One structure value per `*.bookshop.yml`, merged into the build's `_cloudcannon/info.json`, with `id_key: _bookshop_name`. The blueprint becomes `value`, with `_bookshop_name` added.
- **Blueprint rewrites.** An array of objects becomes an empty array plus a generated structure for its items, using the first item as the item's value. A `bookshop:<name>` slot becomes an object input pointing at a generated `_structures._bookshop_single_component_<name>`, with a `null` value. A `!` suffix (`bookshop:button!`) starts it filled in instead.
- **Live editing.** Every built page containing Bookshop's HTML comments gets the live-editing script, and `_cloudcannon/bookshop-live.js` is built.
- **Thumbnails** are copied into the output and set on the structure values.
It fails without `_cloudcannon/info.json` in the output, so it only runs on CloudCannon, not locally.
## Live editing
Bookshop wraps each component call in HTML comments at build time. In the Visual Editor, the live engine reads those comments, re-renders the components whose data changed, and swaps the new markup into the page.
- **Only the library reaches the editor.** The live engine renders library components and shared helpers; site templates outside the library, and helpers the site registers with the SSG, aren't available. The SSG's `bookshop.md` says what can be added.
- **Data passes straight through.** Logic in a layout between front matter and the component call (reassigning, filtering) breaks live editing for that component. Put the logic inside the component.
- **Site data needs `data_config`** in `cloudcannon.config.yml` to be available to live renders.
- **Templates can branch on live editing** through the SSG's flag (`env_bookshop_live`, `ENV_BOOKSHOP_LIVE`) — for code that can't run in the browser.
## Visual data bindings
Bookshop makes each top-level component clickable in the editor by adding `data-cms-bind` attributes to its elements **in the browser**, not in the built HTML. Clicking one opens that component's data in the sidebar. `--disable-bindings` on `@bookshop/generate` turns them off.
`data-cms-bind` or `data-cms-edit` in the site's source was written by hand — CloudCannon's older visual data bindings, independent of Bookshop.
## Shared helpers
A library also has `shared/<ssg>/` templates — helpers rendered by name rather than selected by `_bookshop_name`. The usual one is `page`, which loops over `content_blocks` and renders each block as its component. Arrays of components have to render through a helper or a component for adding and reordering to work live.
## Versions
**MUST:** keep every Bookshop package at the same version — the npm packages, and the Hugo module or Jekyll gem — pinned exactly.
**Why:** they release together. The live engine comes from the site's `node_modules` while the rest comes with `@bookshop/generate`, and a mismatch logs a version error in the editor's console.
`npx @bookshop/up@latest` upgrades all of them together.
## Component browser
`npx @bookshop/browser` serves a local browser of every component with its `preview` data. A page can embed it; `@bookshop/generate` then hosts it on CloudCannon. It is a development aid — nothing on the site depends on it.
eleventy/bookshop.md
# Bookshop in Eleventy
Eleventy's delta from [../bookshop.md](../bookshop.md), which owns the concepts. Read that first.
## Where things live
| Piece | Location |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Library | `component-library/`, or wherever `bookshopLocations` points — the path is relative to the Eleventy input directory |
| Config | `<library>/bookshop/bookshop.config.{js,cjs}`, naming `@bookshop/eleventy-engine` |
| Components | `<library>/components/<path>/<name>.eleventy.liquid` (or the flat `components/<path>.eleventy.liquid`) |
| Shared helpers | `<library>/shared/eleventy/<name>.eleventy.liquid` |
| Plugin | The Eleventy config: `eleventyConfig.addPlugin(pluginBookshop({ bookshopLocations: ["component-library"] }))`, from `@bookshop/eleventy-bookshop` |
| Styles | `bookshop-sass` (from `@bookshop/sass`) compiles every component's `.scss` into one file, run from an npm script or `.cloudcannon/prebuild` |
Bookshop on Eleventy renders Liquid only. A site with Nunjucks templates uses Bookshop from its Liquid files alone.
## Template tags
| Tag | Does |
| -------------------------------------------------------------- | ----------------------------------------------------- |
| `{% bookshop "hero" title: page_title %}` | Renders component `hero` with those props |
| `{% bookshop "hero" bind: hero %}` | Renders `hero` with every key of the object as a prop |
| `{% bookshop "{{block._bookshop_name}}" bind: block %}` | Renders the component the block names |
| `{% bookshop_include "page" content_blocks: content_blocks %}` | Renders shared helper `page` |
| `{% bookshop_component_browser %}` | Embeds the component browser in a page |
A layout's page builder is usually the `page` helper call, with the helper looping over the blocks:
```liquid
{% for block in content_blocks %}
{% bookshop "{{block._bookshop_name}}" bind: block %}
{% endfor %}
```
Components read their props directly: `{{ title }}`.
## Live editing in Eleventy
The live engine is LiquidJS in the browser, holding only the library's files and Bookshop's own `slug`, `url` and `markdownify` filters. Filters, shortcodes and tags from the Eleventy config aren't there unless they are added as LiquidJS plugins through the engine's `plugins` option in `bookshop.config`.
- **Branch** on `env_bookshop_live` for anything the editor can't run — image shortcodes, plugin tags, filters the library doesn't define.
- **Write** the dynamic name as `"{{block._bookshop_name}}"`, with no spaces inside the braces, and pass the data with `bind:`. The live engine reads the name up to the first space, so `"{{ block._bookshop_name }}"` builds but never renders live.
- **Read** site data and `collections` from inside the component, not from a layout that passes them in. Give each data file a `data_config` entry. Collections carry front matter only — no `templateContent`.
## Adding a component
`npx @bookshop/init --component <path>`, run in the library root, creates `<name>.eleventy.liquid`, `<name>.bookshop.yml` and `<name>.scss`. Then follow [../maintaining.md § Adding a component](../maintaining.md#adding-a-component).
## Upgrading
`npx @bookshop/up@latest` moves every `@bookshop/*` package, `@bookshop/eleventy-bookshop` included, to one version.
eleventy/migrating-to-editable-regions.md
# Migrating to editable regions (Eleventy)
Eleventy's delta from [../migrating-to-editable-regions.md](../migrating-to-editable-regions.md), which owns the step order, the structure mapping, the `_bookshop_name` rename and the removal checklist. Read that first.
`<includes>` below is the Eleventy includes directory: `_includes/` under the input directory unless the config sets `dir.includes`.
## What each step means in Eleventy
| Step in the base file | Eleventy work |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Baseline | Baseline on Eleventy 3 and Node 20.19 or later, which the plugin needs. On an older Eleventy, ask the user before upgrading — [cloudcannon-visual-editing/eleventy/visual-editing.md § Setup steps — Liquid path](../../cloudcannon-visual-editing/eleventy/visual-editing.md#setup-steps--liquid-path) |
| Structures | The library root to copy into the scratch directory is the `bookshopLocations` directory. Write each value to a file beside its include — [§ Structure files](#structure-files) |
| Rename, resolve and dispatch — one step | [Install the plugin](#install-the-plugin-before-the-rewrite) first, then rename `_bookshop_name` → `_type`, [move the components](#move-components-into-the-includes-directory) and [rewrite the Bookshop tags](#rewrite-the-bookshop-tags). Region attributes wait for step 5 |
| Remove Bookshop | Replace `bookshop-sass` — [§ Component styles](#component-styles) — then [remove the Bookshop plugin](#remove-the-bookshop-plugin) |
| Add regions | Finish the setup in [cloudcannon-visual-editing/eleventy/visual-editing.md](../../cloudcannon-visual-editing/eleventy/visual-editing.md) — stubs and the script tag; the plugin is already installed. Then [§ Before adding regions](#before-adding-regions) |
## Install the plugin before the rewrite
**MUST:** install `@cloudcannon/editable-regions` and add its Eleventy plugin before rewriting any `{% bookshop %}` tag — steps 3 and 4 of [cloudcannon-visual-editing/eleventy/visual-editing.md § Setup steps — Liquid path](../../cloudcannon-visual-editing/eleventy/visual-editing.md#setup-steps--liquid-path).
**Why:** the rewritten tags use `{% includeWith %}`, which the plugin registers. Without it the build fails on an unknown tag.
The plugin also writes `register-components.js` into the output. Leave that file out of the baseline diff.
## Move components into the includes directory
**MUST:** move each `<library>/components/<path>/<name>.eleventy.liquid` to `<includes>/<path>.liquid`, keeping the grouped path.
**Why:** `_type` is both the structure key and the include path, and the plugin resolves `data-component` as an include path — [cloudcannon-visual-editing/eleventy/visual-editing-reference.md § Component names are include paths](../../cloudcannon-visual-editing/eleventy/visual-editing-reference.md#component-names-are-include-paths). Keeping the Bookshop path keeps every content value valid.
| Bookshop file | Include | `_type` |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------ |
| `component-library/components/home/hero/hero.eleventy.liquid` | `_includes/home/hero.liquid` | `home/hero` |
| `component-library/components/left-right/left-right.eleventy.liquid` | `_includes/left-right.liquid` | `left-right` |
| `component-library/components/<path>/<name>.scss` | The site's Sass directory, keeping the grouped path — [§ Component styles](#component-styles) | — |
| `component-library/shared/eleventy/page.eleventy.liquid` | Delete — the layout's loop replaces it | — |
| `component-library/shared/eleventy/<name>.eleventy.liquid` | `_includes/<name>.liquid` | — |
**Use** the `.liquid` extension, not `.html`. A name without an extension resolves to a `.liquid` include only; an `.html` include would need `.html` in every `_type` value.
**Check** each target path before moving. If an include already exists there, stop and ask the user: renaming a component means rewriting every content value that names it.
## Rewrite the Bookshop tags
| Bookshop | Editable regions |
| ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{% bookshop_include "page" content_blocks: content_blocks %}` and the `page` helper's loop | `{% for block in content_blocks %}{% includeWith block._type, block %}{% endfor %}` in the layout. The region attributes from [§ Page builder blocks in Eleventy](../../cloudcannon-visual-editing/eleventy/visual-editing-reference.md#page-builder-blocks-in-eleventy) come at step 5 |
| `{% bookshop "{{block._bookshop_name}}" bind: block %}` anywhere else | `{% includeWith block._type, block %}` |
| `{% bookshop "hero" bind: hero %}` | `{% includeWith "hero", hero %}` |
| `{% bookshop "image" image_path: page_image %}` | `{% include "image", image_path: page_image %}` |
| `{% bookshop "hero" bind: hero heading: page_title %}` — `bind` plus extra props | **Reshape** so the component's data is one stored object, or use a plain `{% include %}`. `includeWith` spreads one object and drops anything else — [§ The include's data is the `data-prop` value](../../cloudcannon-visual-editing/eleventy/visual-editing-reference.md#the-includes-data-is-the-data-prop-value) |
| `{% bookshop_include "<helper>" … %}` | `{% include "<helper>", … %}` |
| `{% if env_bookshop_live %}` | `{% if ENV_CLIENT %}` — [§ `ENV_CLIENT` in Eleventy](../../cloudcannon-visual-editing/eleventy/visual-editing-reference.md#env_client-in-eleventy) |
| A page holding `{% bookshop_component_browser %}` | Delete the page and its stylesheet |
`includeWith` takes a variable or property, never an inline object. A markdown filter named `renderContent` must be renamed now: the plugin reserves the name, and the editor would show raw markdown — [§ Reserved names](../../cloudcannon-visual-editing/eleventy/visual-editing-reference.md#reserved-names).
Bookshop's bindings made components clickable without any attributes, so a rewritten tag has no regions yet. Add them per the census at step 5.
## Structure files
Write each structure value to `<includes>/<path>.cloudcannon.structure-value.yml`, beside its include, and collect them per structure with `values_from_glob`:
```yaml
_structures:
content_blocks:
values_from_glob:
- /src/_includes/sections/*.cloudcannon.structure-value.yml
hero_blocks:
values_from_glob:
- /src/_includes/heroes/*.cloudcannon.structure-value.yml
```
- **Scope** each glob to its components' directories with a non-recursive `*`. A `**` glob pulls in nested components, such as buttons, that belong to other structures.
- **List** a component that joins several `spec.structures` by its own file path in each structure's `values_from_glob`. Don't copy the file.
- **Use** `values_from_glob`, not `_structures_from_glob`. `_structures_from_glob` loads whole named structures from a file, not one value per component, and pointing it at structure-value files leaves the Add modal empty — [structures.md § `values_from_glob` vs `_structures_from_glob`](../../cloudcannon-configuration/structures.md#values_from_glob-vs-_structures_from_glob).
## Component styles
`bookshop-sass` imported the library's `shared/styles/**/*.scss` first, then `components/**/*.scss`, each sorted by path, compiled them, and wrapped the output in `@media all, bookshop { … }`. Replace it so the compiled rules are unchanged:
- **Move** each component's `.scss` into the site's Sass directory, keeping the grouped path.
- **Write** an index that `@import`s the shared styles, then the components, in Bookshop's order, and import it from the site's Sass entry.
- **Add** `sass` as an explicit dev dependency. It often arrived only through `@bookshop/sass`; without it, the build's `sass` script may run a different global install.
- **Diff** the compiled CSS against the baseline. The only change should be the missing `@media all, bookshop` wrapper.
## Remove the Bookshop plugin
- [ ] Remove `addPlugin(pluginBookshop(…))` and its `require` or `import` from the Eleventy config.
- [ ] Remove `bookshop-sass` and `@bookshop/generate` from npm scripts, `.cloudcannon/prebuild` and `.cloudcannon/postbuild`.
- [ ] Remove every `@bookshop/*` package, `@bookshop/eleventy-bookshop` included.
- [ ] Delete the library directory once it is empty, and drop it from any `addPassthroughCopy`, `addWatchTarget` or ignore list.
- [ ] Run `grep -ri bookshop . --exclude-dir=node_modules --exclude-dir=_site`. Nothing should remain.
## Before adding regions
Bookshop's live engine rendered each component with the data its tag passed. The editable-regions plugin renders an include with only the data at its `data-prop`. Check every former component for these before writing region attributes:
- **Parent-scope reads.** A variable the include uses but its tag never passed works in the build and is `undefined` in the editor — [§ The include's data is the `data-prop` value](../../cloudcannon-visual-editing/eleventy/visual-editing-reference.md#the-includes-data-is-the-data-prop-value).
- **Data built in the template.** An object assembled with `capture` and a YAML filter, then passed to a component, has no data path. Store the shape the component needs in content and in the structure value instead.
- **Helpers the editor can't run.** Image shortcodes and Node-only packages need a stub and an `ENV_CLIENT` branch — [§ The config runs in the browser](../../cloudcannon-visual-editing/eleventy/visual-editing-reference.md#the-config-runs-in-the-browser).
eleventy/overview.md
# Bookshop (Eleventy) — entry point
Eleventy-specific Bookshop guidance. The cross-SSG rules live one level up; the files here carry only what differs for Eleventy.
**Two differences shape everything else:**
- **Bookshop on Eleventy is Liquid only, and so are component regions.** Every Bookshop component is a `.eleventy.liquid` file, so every one can become a Liquid include that the editable-regions plugin re-renders. No Nunjucks rules apply to a former Bookshop library.
- **Editable regions resolve components as include paths.** Migrating moves every component into the includes directory under the same path, so `_type` stays the component name and the include path at once.
## Reading order
| Order | File | Read when |
| ----- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| 1 | [../bookshop.md](../bookshop.md) | Always — the pieces of a Bookshop site |
| 2 | [bookshop.md](bookshop.md) | Always — where they live in Eleventy, and the tags |
| 3 | [../maintaining.md](../maintaining.md) | The site stays on Bookshop |
| 3 | [../migrating-to-editable-regions.md](../migrating-to-editable-regions.md) | The site is migrating — the step order and cross-SSG mapping |
| 4 | [migrating-to-editable-regions.md](migrating-to-editable-regions.md) | The site is migrating — Eleventy's moves, tag rewrites and plugin swap |
| 5 | [troubleshooting.md](troubleshooting.md) | An Eleventy symptom, after checking the generic table |
eleventy/troubleshooting.md
# Troubleshooting (Eleventy)
Eleventy rows only. Check [../troubleshooting.md](../troubleshooting.md) first.
| Symptom | Cause | Fix |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Page-builder blocks build but never update live in the editor | The dynamic name has spaces inside the braces: `"{{ block._bookshop_name }}"` | Write `"{{block._bookshop_name}}"` — [bookshop.md § Live editing in Eleventy](bookshop.md#live-editing-in-eleventy) |
| A component renders empty or errors only in the editor | It uses a filter, shortcode or tag from the Eleventy config, which the live engine doesn't have | Branch on `env_bookshop_live`, or move the logic into the component — [bookshop.md § Live editing in Eleventy](bookshop.md#live-editing-in-eleventy) |
| Build error after a tag rewrite: unknown tag `includeWith` | The editable-regions plugin isn't added yet | [migrating-to-editable-regions.md § Install the plugin before the rewrite](migrating-to-editable-regions.md#install-the-plugin-before-the-rewrite) |
| The Add modal is empty after moving structures into files | The files are loaded with `_structures_from_glob` | Use `values_from_glob` — [migrating-to-editable-regions.md § Structure files](migrating-to-editable-regions.md#structure-files) |
| A migrated component shows raw markdown in the editor | The site's markdown filter is named `renderContent`, which the plugin reserves | Rename it — [cloudcannon-visual-editing/eleventy/visual-editing-reference.md § Reserved names](../../cloudcannon-visual-editing/eleventy/visual-editing-reference.md#reserved-names) |
| `sass` fails, or compiles differently, after removing Bookshop | `sass` came only through `@bookshop/sass` | Add `sass` as a dev dependency — [migrating-to-editable-regions.md § Component styles](migrating-to-editable-regions.md#component-styles) |
hugo/bookshop.md
# Bookshop in Hugo
Hugo's delta from [../bookshop.md](../bookshop.md), which owns the concepts. Read that first.
## Where things live
| Piece | Location |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Library | `component-library/`, with its own `config.toml` and `go.mod` — it is a Hugo module |
| Config | `component-library/bookshop/bookshop.config.cjs`, naming `@bookshop/hugo-engine` |
| Components | `component-library/components/<path>/<name>.hugo.html` (or the flat `components/<path>.hugo.html`) |
| Shared helpers | `component-library/shared/hugo/<name>.hugo.html` |
| Module wiring | The site config: a `replacements` line pointing a local module path at `../component-library`, then imports of that path and `github.com/cloudcannon/bookshop/hugo/v3` |
| Module version | `go.mod` — `github.com/cloudcannon/bookshop/hugo/v3 vX.Y.Z`, matching the npm packages |
The library's `config.toml` mounts itself at `layouts/partials/bookshop` and `assets/bookshop`, which is how the site's templates reach it.
## Template calls
| Call | Does |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `{{ partial "bookshop" (slice "hero" (dict "title" .Params.title)) }}` | Renders component `hero` with those props |
| `{{ partial "bookshop" . }}` | Renders the component named by `._bookshop_name`, with the map as its props |
| `{{ partial "bookshop_partial" (slice "page" .Params.content_blocks) }}` | Renders shared helper `page` |
| ``{{ partial "bookshop_bindings" `.Params.content_blocks` }}`` | Connects the next call to live editing. The argument is a **string** repeating the next call's data expression. Only needed in site layouts, not inside components |
| `{{ $f := partial "bookshop_scss" . }}` | Returns every component's `.scss` (shared styles first), for the site's Sass pipeline |
A layout's page builder is usually the pair:
```go-html-template
{{ partial "bookshop_bindings" `.Params.content_blocks` }}
{{ partial "bookshop_partial" (slice "page" .Params.content_blocks) }}
```
Components read their props directly: `{{ .title }}`.
## Live editing in Hugo
The live engine is real Hugo compiled to WebAssembly, holding only the library's files.
- **Branch** on `site.Params.env_bookshop_live` for anything the editor can't run — `resources.Get` returns nil and `site.GetPage` is unavailable there.
- **Read** data through `site.Data`, not `.Site.Data`, and give the data file a `data_config` entry.
- **Add** a site partial or shortcode the library needs through `extraFiles` on the engine in `bookshop.config.cjs`.
- **Keep** HTML comments when minifying: `minify.tdewolff.html.keepComments: true`. Without it Bookshop's markers are stripped and live editing never connects.
## Adding a component
`npx @bookshop/init --component <path>`, run in `component-library/`, creates `<name>.hugo.html`, `<name>.bookshop.yml` and `<name>.scss`. Then follow [../maintaining.md § Adding a component](../maintaining.md#adding-a-component).
## Upgrading
`npx @bookshop/up@latest` moves the npm packages and the Hugo module together (it runs `hugo mod get` for you). A Bookshop Hugo site is often pinned to an old Hugo — check the site's Hugo version window before upgrading either.
hugo/migrating-to-editable-regions.md
# Migrating to editable regions (Hugo)
Hugo's delta from [../migrating-to-editable-regions.md](../migrating-to-editable-regions.md), which owns the step order, the structure mapping, the `_bookshop_name` rename and the removal checklist. Read that first.
## What each step means in Hugo
| Step in the base file | Hugo work |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Baseline | Baseline on the Hugo the site will ship. If local Hugo and the site's `hugo_version` differ, or the pin is old, ask the user which version to target first — [cloudcannon-visual-editing/hugo/visual-editing.md § Upgrading Hugo first](../../cloudcannon-visual-editing/hugo/visual-editing.md#upgrading-hugo-first) |
| Rename, resolve and dispatch — one step | Rename `_bookshop_name` → `_type`, [move the components](#move-components-into-layoutspartials), and [rewrite the Bookshop calls](#rewrite-the-bookshop-template-calls). Region attributes wait for step 5 |
| Remove Bookshop | Replace `bookshop_scss` — [§ Component styles](#component-styles) — then [swap the modules](#swap-the-modules). Both before the Bookshop module goes, since `bookshop_scss` comes from it |
| Add regions | Follow [cloudcannon-visual-editing/hugo/overview.md](../../cloudcannon-visual-editing/hugo/overview.md) — the module is already imported |
## Move components into `layouts/partials/`
**MUST:** move each `component-library/components/<path>/<name>.hugo.html` to `layouts/partials/<path>.html`, keeping the grouped path.
**Why:** `_type` is both the structure key and the partial path, and the editor resolves `data-component` against `layouts/partials/`. Keeping the Bookshop path keeps every content value valid.
| Bookshop file | Partial | `_type` |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------ |
| `component-library/components/home/hero/hero.hugo.html` | `layouts/partials/home/hero.html` | `home/hero` |
| `component-library/components/left-right/left-right.hugo.html` | `layouts/partials/left-right.html` | `left-right` |
| `component-library/components/<path>/<name>.scss` | `assets/scss/components/<path>.scss`, keeping the grouped path — see [§ Component styles](#component-styles) | — |
| `component-library/shared/hugo/page.hugo.html` | Delete — the page layout's `range` replaces it | — |
## Rewrite the Bookshop template calls
| Bookshop | Editable regions |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ``{{ partial "bookshop_bindings" `.Params.content_blocks` }}`` + `{{ partial "bookshop_partial" (slice "page" .Params.content_blocks) }}`, or `(slice "page" .)` inside `with .Params.content_blocks` | `{{ range .Params.content_blocks }}{{ partial ._type . }}{{ end }}`. The region attributes from [cloudcannon-visual-editing/hugo/visual-editing-reference.md § Page builder blocks in Hugo](../../cloudcannon-visual-editing/hugo/visual-editing-reference.md#page-builder-blocks-in-hugo) come at step 5 |
| `{{ partial "bookshop" . }}` (dispatch on `._bookshop_name`) | `{{ partial ._type . }}` |
| `{{ partial "bookshop" (slice "processed-image" $props) }}` | `{{ partial "processed-image" $props }}` |
| ``{{ partial "bookshop_bindings" `…` }}`` before a standalone component in a page template | Remove it, and call `{{ partial "<path>" <data> }}` with the data the region will name. The `<editable-component>` wrapper comes at step 5 |
| `{{ partial "bookshop" (slice "<name>" (dict "Params" .Params.x "tags" .Params.tags)) }}` | **Reshape the partial** so its context is exactly the `data-prop` value: `{{ partial "<name>" .Params.x }}`, reading anything else (taxonomies, page fields) through `page.Params` — [cloudcannon-visual-editing/hugo/visual-editing-reference.md § The partial's context](../../cloudcannon-visual-editing/hugo/visual-editing-reference.md#the-partials-context-is-the-data-prop-value) |
| `site.Params.env_bookshop_live` | `site.Params.ENV_CLIENT` |
| `{{ $files := partial "bookshop_scss" . }}` in `<head>` | See [§ Component styles](#component-styles) |
Bookshop's bindings made components clickable without any attributes, so a rewritten call has no regions yet. Add them per the census — [cloudcannon-visual-editing/hugo/visual-editing.md](../../cloudcannon-visual-editing/hugo/visual-editing.md).
## Component styles
`bookshop_scss` concatenated every component's `.scss` in `resources.Match` order. Replace it so the compiled CSS is unchanged:
- **Read** the order from the baseline build: each component's rules appear in the built `bookshop*.css` in the order Bookshop matched them, shared styles first. Don't sort the paths yourself — Hugo's order is not byte order (`heroes/hero` comes before `heroes/hero-video`).
- **SCSS site** — `@import` each moved file from the site's SCSS entry, in that order. Order changes which rule wins.
- **Tailwind or plain-CSS site** — the main stylesheet can't import SCSS. Give the components their own entry (`assets/scss/components.scss`) compiled with `css.Sass`, linked beside the main stylesheet.
- **Diff** the compiled CSS against the baseline. It must be identical at this point.
Bookshop components often emit a per-instance `<style>` block with interpolated values. Move the rule into the component's stylesheet and pass the value as a custom property on the root (`style="--hover-brightness: {{ .hover_brightness }}"`). That keeps the partial single-root for its array-item wrapper, and two instances no longer override each other — the old global `<style>` let the last instance win everywhere. Diff the CSS again afterwards: the only change should be the moved rules.
## Swap the modules
Remove both Bookshop module imports and the local component-library replacement from the site config, then import editable-regions. YAML:
```yaml
# remove
module:
replacements: local/component-library -> ../component-library
imports:
- path: local/component-library
- path: github.com/cloudcannon/bookshop/hugo/v3
# add
module:
imports:
- path: github.com/CloudCannon/editable-regions
```
TOML:
```toml
# remove
[module]
replacements = "local/component-library -> ../component-library"
[[module.imports]]
path = 'local/component-library'
[[module.imports]]
path = 'github.com/cloudcannon/bookshop/hugo/v3'
# add
[[module.imports]]
path = 'github.com/CloudCannon/editable-regions'
```
- [ ] Run `hugo mod get github.com/CloudCannon/editable-regions@latest`, then `hugo mod tidy` to drop Bookshop from `go.mod` and `go.sum`.
- [ ] Delete `component-library/` (it may have its own `config.toml` and `go.mod`).
- [ ] Remove the npm scripts that ran `@bookshop/browser` alongside `hugo server`.
hugo/overview.md
# Bookshop (Hugo) — entry point
Hugo-specific Bookshop guidance. The cross-SSG rules live one level up; the files here carry only what differs for Hugo.
**Two differences shape everything else:**
- **The library is a Hugo module.** `component-library/` is mounted into `layouts/partials/bookshop`, and the site reaches components through Bookshop's partials — `partial "bookshop"`, `bookshop_partial`, `bookshop_bindings`.
- **Editable regions resolve components as partials.** Migrating moves every component into `layouts/partials/` under the same path, so `_type` stays the component name and the partial path at once.
## Reading order
| Order | File | Read when |
| ----- | -------------------------------------------------------------------------- | -------------------------------------------------------------- |
| 1 | [../bookshop.md](../bookshop.md) | Always — the pieces of a Bookshop site |
| 2 | [bookshop.md](bookshop.md) | Always — where they live in Hugo, and the template calls |
| 3 | [../maintaining.md](../maintaining.md) | The site stays on Bookshop |
| 3 | [../migrating-to-editable-regions.md](../migrating-to-editable-regions.md) | The site is migrating — the step order and cross-SSG mapping |
| 4 | [migrating-to-editable-regions.md](migrating-to-editable-regions.md) | The site is migrating — Hugo's moves, rewrites and module swap |
| 5 | [troubleshooting.md](troubleshooting.md) | A Hugo symptom, after checking the generic table |
hugo/troubleshooting.md
# Troubleshooting (Hugo)
Hugo rows only. Check [../troubleshooting.md](../troubleshooting.md) first.
| Symptom | Cause | Fix |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Build error: `📚 Error from Bookshop: Component "x" does not exist` | No `components/x/x.hugo.html` or `components/x.hugo.html` — often a shared helper called with `partial "bookshop"` | Fix the name; call helpers with `bookshop_partial` |
| Build error: "bookshop_bindings expected a string" | The data was passed as a value | Wrap the expression in backticks: ``partial "bookshop_bindings" `.Params.x` `` |
| Build error on an image inside a component directory | The library's `config.toml` mount lacks `includeFiles = ["**/*.hugo.html"]` | Add it, or run `npx @bookshop/up@latest` |
| Component lookups fail after upgrading Hugo past 0.146 | Hugo lowercases template names; older Bookshop can't find mixed-case components | Upgrade Bookshop to 3.18.3 or later |
| Live editing never connects on a site built with `--minify` | Minification strips Bookshop's HTML comments | `minify.tdewolff.html.keepComments: true` |
| A component renders empty or errors only in the editor | It calls `resources.Get`, `site.GetPage` or a site partial outside the library | Branch on `site.Params.env_bookshop_live`, or add the partial through `extraFiles` — [bookshop.md § Live editing in Hugo](bookshop.md#live-editing-in-hugo) |
| `.Site.Data` is empty in the editor | Only `site.Data` is populated live | Use `site.Data`, and add a `data_config` entry |
| Editor console: "[hugo-engine] No component found for X" | The name isn't in the library bundle — usually a site partial | Move it into the library, or add it to `extraFiles` |
| The editor is slow or freezes while typing | Bookshop before 3.19 rendered Hugo on the main thread | Upgrade to 3.19 or later |
maintaining.md
# Maintaining a Bookshop site
Changing a site that stays on Bookshop. Read [bookshop.md](bookshop.md) first for the pieces; the SSG's `bookshop.md` has the template syntax.
**MUST NOT:** migrate as a side effect. A component change on a Bookshop site is made the Bookshop way.
## Adding a component
- **Create** the component in the library: `npx @bookshop/init --component <path>` from the library root creates the template, `<name>.bookshop.yml` and `<name>.scss`. Some sites ship their own script instead (`npm run new-component`) — use it if there is one.
- **Put** every prop the template reads into `blueprint`, with the value a new instance should start with.
- **Add** the structure key the page's array uses to `spec.structures` — usually `content_blocks`. Check the matching input points at `_structures.<key>` in `cloudcannon.config.yml`.
- **Add** `_inputs` for any prop whose type CloudCannon would guess wrong — selects, images, markdown.
- **Render** it through the existing dispatcher or `page` helper. No layout change is needed for a block in `content_blocks`.
- **Build** locally. The new block appears in the editor's Add menu only after a CloudCannon build, because `@bookshop/generate` makes the structures there.
## Changing a component
| Change | Watch out for |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------ |
| A new prop | Add it to `blueprint`. Existing pages don't have it — guard it in the template |
| A renamed or removed prop | Existing front matter keeps the old key. Rewrite the content files, or keep reading both |
| A renamed or moved component | Its name is stored in every page's `_bookshop_name`. Rewrite every value, or don't rename |
| A nested component slot | `bookshop:<name>` starts empty (`null`); guard it in the template. `bookshop:<name>!` starts filled in |
| An array of objects in a blueprint | The first item becomes the template for new items; the array starts empty on new components |
**Blueprint changes affect new components only.** Nothing migrates existing front matter.
## Code that can't run live
The live engine renders in the browser, with only the library available. When a component needs something the editor can't provide — an asset pipeline, a page lookup, a site template outside the library — branch on the SSG's live-editing flag and render a stand-in:
- **Branch** on the flag — see the SSG's `bookshop.md` for its name and for what else the live engine lacks.
- **Turn off** live rendering for one top-level call with `live_render: false` (also `_live_render` in a blueprint). It doesn't reach nested components.
- **Pass** `--skip-live` to `@bookshop/generate` to turn live editing off site-wide, or `--disable-bindings` to keep live rendering without the click-to-edit overlays.
## Upgrading Bookshop
- [ ] Run `npx @bookshop/up@latest` — it moves every npm package (the Eleventy plugin among them), the Hugo module or the Jekyll gem to one version, and runs Bookshop's own migrations.
- [ ] Check `package.json` pins every `@bookshop/*` package to the same exact version.
- [ ] Build locally, then on CloudCannon, and check the editor's console for Bookshop's version error.
## Checklist
- [ ] Every prop the template reads is in `blueprint`
- [ ] `spec.structures` names the key the page's array input uses
- [ ] Every `@bookshop/*` package, and the SSG plugin, are on one version
- [ ] `.cloudcannon/postbuild` still runs `npx @bookshop/generate`
- [ ] The component renders in a local build, and live in the editor after a CloudCannon build
migrating-to-editable-regions.md
# Migrating to editable regions
Replace Bookshop with `@cloudcannon/editable-regions` on a site that uses Bookshop components. This file owns the step order and the cross-SSG mapping. What differs per SSG lives in that SSG's `migrating-to-editable-regions.md` — [Astro](astro/migrating-to-editable-regions.md), [Hugo](hugo/migrating-to-editable-regions.md), [Eleventy](eleventy/migrating-to-editable-regions.md), [Next.js](nextjs/migrating-to-editable-regions.md).
**MUST:** check the SSG's row in [SKILL.md § SSG support](SKILL.md#ssg-support) first. If editable regions can't re-render that SSG's components, stop — the site stays on Bookshop; see [maintaining.md](maintaining.md).
## Steps
Run these in order. Each ends with a build that matches the baseline.
| # | Step | Work |
| --- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1 | Baseline | Build the unmodified site on the target SSG version — [§ Baseline](#baseline) |
| 2 | Structures | Download the configuration schemas and read [structures.md](../cloudcannon-configuration/structures.md) first — [cloudcannon-configuration § Do this before writing any configuration](../cloudcannon-configuration/SKILL.md#do-this-before-writing-any-configuration). Then write `_structures` from what Bookshop generated — [§ Writing `_structures`](#writing-_structures-from-bookshopyml) |
| 3 | Rename, resolve and dispatch — one step | Rename `_bookshop_name` ([§ Renaming](#renaming-_bookshop_name)), make every component resolvable by the SSG under its name, and replace the Bookshop dispatcher with a plain native loop. **Any one alone breaks the build** — Bookshop's dispatcher selects on `_bookshop_name`. No `data-editable` attributes yet — they follow the census in step 5 |
| 4 | Remove Bookshop | [§ Removing Bookshop](#removing-bookshop). Do it now, not at the end: while Bookshop is installed it can still rewrite components and supply dependencies, so later builds prove less |
| 5 | Add regions | Install the SSG's editable-regions integration, then run the census and add regions as for any site — [cloudcannon-visual-editing](../cloudcannon-visual-editing/SKILL.md), with [§ Regions on a former Bookshop site](#regions-on-a-former-bookshop-site) |
| 6 | Verify | Diff against the baseline, then try the editor locally — [cloudcannon-dev-server](../cloudcannon-dev-server/SKILL.md) |
**Inside `migrate-to-cloudcannon`** — the site is joining CloudCannon in the same task — the steps land in its phases: step 1 in Phase 1, step 2 in Phase 2, steps 3 and 4 in Phase 3, step 5 in Phase 4, step 6 in Phase 5.
**Large sites** — run the steps in subagents as [migrate-to-cloudcannon/orchestration.md](../migrate-to-cloudcannon/orchestration.md) describes. Step 3 is one unit: don't split it.
## Baseline
- **Record** the page count, every page's visible text and `<img>` count, inline `style` attributes, and the compiled CSS.
- **Build** the unmodified site twice from a clean cache (delete the SSG's cache and output directories first), or once more in a fresh copy — an in-place rebuild can hide order changes. Compare a paginated listing whose order changes (posts sharing a date) as one set across all its pages; a "first N" of an unsorted collection only by item count.
- **Diff** against this after every later step, not only at the end.
- **On Tailwind v4**, automatic source detection scans every file git doesn't ignore — templates, `.cloudcannon/migration/` notes, downloaded schemas. A utility appearing or vanishing in the CSS diff is usually a word added to or removed from a scanned file. Exclude `.cloudcannon/` with `@source not` before the baseline.
## What Bookshop did that you now do explicitly
| Bookshop did | Replace with |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Rendered components live in the editor through its engine | Component regions. The component must be resolvable by the SSG's editable-regions integration — see [editable-regions.md § EditableComponent](../cloudcannon-visual-editing/editable-regions.md#editablecomponent) |
| Made every component clickable through bindings | Explicit `data-editable` regions on every field and list. Run the section census as for any site |
| Synthesised `_structures` from `*.bookshop.yml` at build time (`@bookshop/generate`) | Hand-authored `_structures` in `cloudcannon.config.yml` — see [§ Writing `_structures` from `*.bookshop.yml`](#writing-_structures-from-bookshopyml) |
| Selected a component with `_bookshop_name` | `_type` as the array's `data-component-key`; it is `id_key`'s default, so structures need no `id_key` — see [§ Renaming `_bookshop_name`](#renaming-_bookshop_name) |
| Passed props its dispatcher computed (an index, a count, the page body) | Region attributes on the component's wrapper — the SSG file says which |
| Exposed a live-editing flag to templates | The SSG's `ENV_CLIENT` — see [visual-editing-reference.md § Detecting the editor](../cloudcannon-visual-editing/visual-editing-reference.md#detecting-the-editor-and-skipping-build-only-logic) |
| Collected co-located component styles (Hugo, Jekyll, Eleventy) | The site's own stylesheet pipeline — the SSG file says how |
## Writing `_structures` from `*.bookshop.yml`
**MUST:** hand-write a structure value for every component before removing the Bookshop build hook.
**Why:** `@bookshop/generate` built `_structures` at build time and they were never committed. The committed config may have `_structures.content_blocks` with no `values`, or inputs pointing at `_structures.<key>` that doesn't exist — delete the hook without writing them and the "Add" modal is silently empty.
### See what Bookshop generated
**MUST:** start from `@bookshop/generate`'s real output, not from reading the `*.bookshop.yml` files.
**Why:** it rewrote blueprints and cascaded `_inputs` into structures it generated for nested arrays. Reading the source files misses both — [bookshop.md § What `@bookshop/generate` does](bookshop.md#what-bookshopgenerate-does).
Run it in a scratch folder at the repo root, outside the site's source folders. Add the folder to `.gitignore` first if nothing ignores it — Tailwind v4 and other tools scan files git doesn't ignore.
```sh
G="$PWD/.bookshop-generate"
mkdir -p "$G/site/_cloudcannon" && echo '{}' > "$G/site/_cloudcannon/info.json"
cp -R <library root> "$G/" # the directory holding bookshop/bookshop.config.cjs — component-library/, or src/ on Astro
(cd "$G" && npx @bookshop/generate@<version> --skip-live --skip-components)
```
Use the site's Bookshop version for `<version>`, as `npm ls` reports it for its `@bookshop/*` packages. When the site has no Bookshop package to read it from, use `latest`.
`$G/site/_cloudcannon/info.json` now holds `_structures` exactly as the editor received them. Delete the folder, and its `.gitignore` entry, once the structure values are written.
### Map each component onto a structure value
Each `*.bookshop.yml` with a non-empty `spec.structures` became one structure value. One with no `spec.structures` is a standalone component and gets none.
| `*.bookshop.yml` key | Structure value key |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `spec.structures: [<key>]` | Which `_structures.<key>` the value goes in |
| `spec.label` | `label` |
| `spec.description` | `description` |
| `spec.icon` | `icon` — validate it against the schema's icon enum |
| `spec.tags` | `tags` |
| Any other `spec.*` key | The same key on the structure value — validate it |
| `blueprint` | `value`, plus the component key (`_type: <component name>`), in the shape the generated output had |
| `_inputs`, any other `_*` | The same key on the structure value — or on an item structure, see below |
| The YAML `preview:` key | Drop it — it only fed Bookshop's component browser |
| `<name>.preview.*` / `<name>.icon.*` image | Move it to a path the site serves and set `picker_preview.gallery.image`, or drop it — Bookshop only copied it into the build |
| Blueprint value | Generated as |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Array of objects | `[]` in `value`, plus an item structure built from the first item, inline on `_inputs.<key>.options.structures`. Write it as a named structure — see [structures.md § Nested item structures](../cloudcannon-configuration/structures.md#nested-item-structures) |
| `bookshop:structure:<key>` | `null` in `value`, `_inputs.<field>: { type: object, options: { structures: _structures.<key> } }` |
| `[bookshop:structure:<key>]` | `[]` in `value`, the same input with `type: array` |
| `bookshop:<component>` / `[bookshop:<component>]` | The same, pointing at a generated `_structures._bookshop_single_component_<component>` — write that structure too |
| Any of the above with `!` | Starts filled in with that component's blueprint instead of empty |
```yaml
_structures:
content_blocks:
values:
- label: Counter
description: Counter section
icon: functions
value:
_type: global/counter
title: Scale your
numbers: []
_inputs:
numbers:
type: array
options:
structures: _structures.counter_numbers
counter_numbers:
values:
- label: Number
value:
number: "200"
text: Venture capital raised
_inputs:
number:
type: text
```
**MUST:** keep the shape of `value` the generated output had — empty arrays, `null` slots. Then add a `preview` to each value as [structures.md](../cloudcannon-configuration/structures.md) requires; Bookshop never generated one.
**Why:** copying the raw blueprint pre-fills every new block with the first array item, and fills slots Bookshop left empty.
Then, for each structure value:
- **Validate** the result against the configuration schema — see [cloudcannon-configuration](../cloudcannon-configuration/SKILL.md). Bookshop never validated `spec.icon` or input options, so invalid values carried over are common: icons not in the enum, `_instance_value` (now `instance_value`), lowercase `instance_value: now` (now `NOW`), inputs with `instance_value` or `hidden` but no `type`, and `_structures.<key>` references to structures that never existed.
- **Place** every `_inputs` key where its field is. Keys may name fields at any depth, or dotted paths. A key that names no field in `value` often names a field of an array item — Bookshop cascaded it into the item structure — so move it there. Delete it only if nothing has the field — see [configuration-gotchas.md § An `_inputs` key that names no field is ignored](../cloudcannon-configuration/configuration-gotchas.md#an-_inputs-key-that-names-no-field-is-ignored).
- **Diff** the keys in content against `value`. Keys content has and the blueprint never did are either dead (no template reads them — leave them out) or live (add them to `value`).
- **Check** blueprint values against the region type that will edit them. Bookshop rendered a bare number (`number: 200`) fine; a text region bound to it shows an error card — see [editable-regions.md § Value types per region](../cloudcannon-visual-editing/editable-regions.md#value-types-per-region).
- **Link** each array input to its structure — see [structures.md](../cloudcannon-configuration/structures.md).
## Renaming `_bookshop_name`
**MUST:** rename the key and keep the value. `_bookshop_name: home/hero` becomes `_type: home/hero`.
**Why:** the value is the component path, and a grouped path (`home/hero`, `about/hero`) keeps components with the same short name apart. Flattening them means renaming components and rewriting every content value.
- **Rename** the key everywhere it appears — content files, schema files, structure values, pages outside any collection, and component code that reads it (nested buttons often do). `grep -rn _bookshop_name . --exclude-dir=node_modules` lists them.
- **Set** `data-component-key="_type"` on each component array. `_type` is `id_key`'s default, so structures, nested ones included, need no `id_key`.
- **Keep** each component resolvable under the same name — the SSG file says how.
**Common miss:** a dispatcher that guards its lookup (`Component && …`) makes a missed rename vanish silently rather than fail the build. Only the baseline diff catches it.
## Removing Bookshop
Do this straight after step 3.
- [ ] Remove `npx @bookshop/generate` from `.cloudcannon/postbuild`. Move every other hook step — in `postbuild` or `prebuild` — into the build command, see [build-commands.md](../cloudcannon-configuration/build-commands.md). If the build script runs steps in parallel (`run-p build:*`), chain the moved steps after it with `&&`. Delete a hook once it is empty.
- [ ] On a site already on CloudCannon, update its build command once the hooks are gone — see [configuration.md § Build settings](../cloudcannon-configuration/configuration.md#build-settings).
- [ ] Remove the SSG's Bookshop plugin, module or integration.
- [ ] Remove the `@bookshop/*` dependencies.
- [ ] Build straight away. A package the site uses but never declared (`sass`, often) may have arrived only through Bookshop — install each missing one as a direct dependency, at the version the baseline resolved (`npm ls <package>` in the baseline), so the output doesn't change.
- [ ] Remove the Bookshop npm scripts (`bookshop`, `update-bookshop`). A `new-component` script writes `*.bookshop.yml` files nothing reads now — remove it, or rewrite it to write a structure value.
- [ ] Remove each `*.bookshop.yml` once its structure value is written, the `bookshop/` config directory, and the component library directory once it is empty.
- [ ] Remove path aliases only the Bookshop dispatcher used (`tsconfig.json`, site config), component-library paths in CSS tooling config (Tailwind `content`, PostCSS), and Bookshop entries in `.gitignore`.
- [ ] Build, and diff against the baseline.
## Regions on a former Bookshop site
The census and checklists in [cloudcannon-visual-editing](../cloudcannon-visual-editing/SKILL.md) apply unchanged. These differ:
- **Bookshop's coverage is not a census.** Its bindings made whole components clickable; every field now needs its own region. Text that was hardcoded under Bookshop gets a census row and the usual treatment decision.
- **Inputs** — give every field that a region edits its own `_inputs` entry: `markdown` with `options` if it holds formatting, `text` or `textarea` if it doesn't. The input sets the region's toolbar — see [configuration-gotchas.md § Set region toolbars on the input](../cloudcannon-configuration/configuration-gotchas.md#set-region-toolbars-on-the-input-not-in-_editablestext-or-block).
- **Data files shared partials read** need `data_config` entries. Bookshop's covered only data its live engine read — check every data file a header, footer or other shared partial reads.
- **Hand-written `data-cms-bind` / `data-cms-edit`** are CloudCannon's older bindings, not Bookshop, and keep working beside regions. Converting them is a census decision. Bookshop's own bindings were added in the browser and never appear in source.
## Common mistakes
| Excuse | Reality |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| "The committed config already has `_structures`" | Check it has `values`, and that the page-builder input links to it. Bookshop generated them at build time. |
| "Bookshop made it editable, so it's editable" | Bookshop's bindings are gone. Every field needs its own region, and every section a census row. |
| "I'll flatten the component names while I'm here" | Every content file stores the name. Keep the grouped path and rename only the key. |
| "The `_inputs` came across from the `.bookshop.yml`, so they're right" | They were never validated, and some belong on array-item structures. Check each key against where its field is. |
| "This `_inputs` key names no field, so I'll delete it" | Check the array items first. Bookshop cascaded component inputs into item structures. |
| "I'll rename `_bookshop_name` now and fix the templates later" | The build breaks the moment the key changes. Rename, resolve and dispatch in one step. |
| "I'll leave Bookshop installed until the regions are done" | It keeps rewriting components and supplying dependencies, so every build until then proves less. Remove it at step 4. |
nextjs/bookshop.md
# Bookshop in Next.js
Next.js's delta from [../bookshop.md](../bookshop.md), which owns the concepts. Read that first.
## Where things live
| Piece | Location |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Library | `component-library/`, holding `bookshop/bookshop.config.js` and `components/<path>/<name>.bookshop.yml` — no templates |
| `bookshop.config.js` | Names no engine; it only marks the library root for `@bookshop/generate` |
| Components | The site's React components, at a path matching the Bookshop name (`components/home/hero.jsx` for `home/hero`) |
| Blocks renderer | A site component that loops over `content_blocks` and picks each block's component by `_bookshop_name` — through a glob import of `components/`, or a map |
| `@bookshop/generate` | `npx @bookshop/generate` in `.cloudcannon/postbuild`; `package.json` may list no `@bookshop/*` package |
| Live editing | `CloudCannonConnect` from `@cloudcannon/react-connector`, in `pages/_app.*` |
| Click-to-edit | `data-cms-bind` on each block's root element, from a binding string the blocks renderer passes in |
## What doesn't apply
- **The SSG plugin.** Bookshop has none for Next.js; the blocks renderer resolves components itself.
- **The live engine and its script.** `@bookshop/generate` finds no Bookshop comments in Next's output, logs "No live editing connected as no pages contained Bookshop components", and writes structures only.
- **Shared helpers, `live_render` and the live-editing flag.** Nothing renders through Bookshop.
- **Version matching.** With no engine or plugin, `@bookshop/generate` is the only Bookshop code that runs.
## Maintaining
[../maintaining.md](../maintaining.md) applies, with these changes:
- **Create** the React component at the path the blocks renderer resolves, as well as the `*.bookshop.yml`.
- **Pass** the component's root element the binding string the renderer provides, as the other components do.
nextjs/migrating-to-editable-regions.md
# Migrating to editable regions (Next.js)
Next.js's delta from [../migrating-to-editable-regions.md](../migrating-to-editable-regions.md), which owns the step order, the structure mapping, the `_bookshop_name` rename and the removal checklist. Read that first.
## What each step means in Next.js
| Step in the base file | Next.js work |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Baseline | Delete `.next/` and `out/` before each build; compare the HTML in `out/`. A build script that runs `next export` fails from Next 14 — set `output: "export"` in the Next config and drop `next export` before the baseline |
| Structures | The library root to copy into the scratch directory is `component-library/`. The generated structure carries `id_key: _bookshop_name`; leave it out, since the rename makes `_type` the key |
| Rename, resolve and dispatch — one step | Rename `_bookshop_name` to `_type`, build the [component map](#replace-the-lookup-with-a-component-map), and [give each component its fields as props](#components-take-the-block-as-props). No `data-editable` attributes yet |
| Remove Bookshop | [§ Removing the setup](#removing-the-setup), with the base checklist |
| Add regions | Follow [cloudcannon-visual-editing/nextjs/overview.md](../../cloudcannon-visual-editing/nextjs/overview.md), starting with [§ Sites using the React connector](../../cloudcannon-visual-editing/nextjs/visual-editing.md#sites-using-the-react-connector) |
Inside `migrate-to-cloudcannon`, the rest of the site — routes, content loading, the export settings — follows [migrate-to-cloudcannon/nextjs/overview.md](../../migrate-to-cloudcannon/nextjs/overview.md).
## Replace the lookup with a component map
The blocks renderer finds each component by its Bookshop name. Replace the lookup with the `blockMap` and `Blocks` renderer in [migrate-to-cloudcannon/nextjs/page-building.md § The block map](../../migrate-to-cloudcannon/nextjs/page-building.md#the-block-map), keyed by the same names.
- **Key** the map by the full Bookshop name, slashes included: `"home/hero"`, `"global/counter"`. Each key is a flat string; the map has no nesting.
- **List** every name in use: `grep -rhoE '_bookshop_name: *[^ ]+' --exclude-dir=node_modules . | sort -u`, plus every component with a structure value. A name content uses with no structure is still a key.
- **Import** each component directly. The map replaces the glob import, so remove the glob loader (`import-glob-keyed` or similar) from `package.json` and its rule from the Next config's `webpack` function. Remove the function once it is empty.
- **Throw** on an unknown `_type`, as the `Blocks` renderer does. A renderer that skips missing components hides a missed rename.
```js
import HomeHero from "../components/home/hero";
import GlobalCounter from "../components/global/counter";
// Keys are the `_type` values in content_blocks.
export const blockMap = {
"home/hero": HomeHero,
"global/counter": GlobalCounter,
};
```
Where the map file goes is in [cloudcannon-visual-editing/nextjs/visual-editing.md § Setup steps](../../cloudcannon-visual-editing/nextjs/visual-editing.md#setup-steps).
## Components take the block as props
**MUST:** give each block component its fields as top-level props, and render it with `<Block {...block} />`.
**Why:** the editor re-renders a registered component with the array item as its props. A component that reads a wrapped `block` prop gets `undefined` there and throws on its first field.
| Component signature | Change |
| -------------------------- | -------------------------------------------------------------------- |
| `({ block, dataBinding })` | `({ dataBinding, ...block })` — the body keeps reading `block.title` |
| `({ block })` | `(block)` |
| `({ title, image, … })` | None |
Until step 5 removes the connector setup, the renderer keeps passing `dataBinding` beside the spread fields, so the built HTML still matches the baseline:
```jsx
<Block {...block} dataBinding={`#content_blocks.${index}`} />
```
A component that also takes props the renderer computed (an index, the page) gets them from region attributes instead — see the base file's [§ What Bookshop did that you now do explicitly](../migrating-to-editable-regions.md#what-bookshop-did-that-you-now-do-explicitly).
## Renaming `_bookshop_name`
The base rule applies: rename the key to `_type` and keep the value. In Next.js the renderer, the component map and the content are all site code and files, so the rename is one search and replace:
- [ ] Rename the key in every content file, schema file and structure value.
- [ ] Rename it in components that read it, such as a nested button choosing its variant.
- [ ] Change the renderer's lookup to `blockMap[block._type]` in the same change.
- [ ] Run `grep -rn _bookshop_name --exclude-dir=node_modules .` and expect no matches.
## Removing the setup
The base checklist applies. On a Next.js site:
- [ ] Remove `npx @bookshop/generate` from `.cloudcannon/postbuild`, then move the hook's other steps per [build-commands.md § `.cloudcannon/` hook files](../../cloudcannon-configuration/build-commands.md#cloudcannon-hook-files).
- [ ] Delete `component-library/` once every `*.bookshop.yml` has its structure value. It holds nothing else.
- [ ] Remove the glob loader and its `webpack` rule, if the map step hasn't.
- [ ] Leave `@cloudcannon/react-connector` and `data-cms-bind` for step 5; the connector section removes the connector and settles each binding.
nextjs/overview.md
# Bookshop (Next.js) — entry point
Next.js-specific Bookshop guidance. The cross-SSG rules live one level up; the files here carry only what differs for Next.js.
Bookshop has no Next.js engine: on a Next.js site it provides `_structures` and nothing else.
**Two differences shape everything else:**
- **The site renders its own components.** The library holds only `*.bookshop.yml` files. The components are the site's React components, and the site's own blocks renderer picks one per block by `_bookshop_name`. Migrating replaces that lookup with a component map keyed by the same names.
- **Live editing comes from the React connector, not Bookshop.** `@cloudcannon/react-connector` re-renders the whole page in the editor, and `data-cms-bind` attributes make blocks clickable. Editable regions replace the connector, and each binding gets a census row.
## Reading order
| Order | File | Read when |
| ----- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| 1 | [../bookshop.md](../bookshop.md) | Always — the pieces of a Bookshop site |
| 2 | [bookshop.md](bookshop.md) | Always — which pieces a Next.js site has, and where they live |
| 3 | [../maintaining.md](../maintaining.md) | The site stays on Bookshop |
| 3 | [../migrating-to-editable-regions.md](../migrating-to-editable-regions.md) | The site is migrating — the step order and cross-SSG mapping |
| 4 | [migrating-to-editable-regions.md](migrating-to-editable-regions.md) | The site is migrating — the component map, component props, removing the setup |
| 5 | [troubleshooting.md](troubleshooting.md) | A Next.js symptom, after checking the generic table |
nextjs/troubleshooting.md
# Troubleshooting (Next.js)
Next.js rows only. Check [../troubleshooting.md](../troubleshooting.md) first.
| Symptom | Cause | Fix |
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Build log: "No live editing connected as no pages contained Bookshop components" | Expected on Next.js — Bookshop renders nothing, and live editing comes from the connector | None |
| A block throws in the editor reading a field of `undefined`, while the build renders it | The component reads a wrapped `block` prop; the editor passes the fields as props | [migrating-to-editable-regions.md § Components take the block as props](migrating-to-editable-regions.md#components-take-the-block-as-props) |
| Build error: "This build is using Turbopack, with a `webpack` config and no `turbopack` config" | Next 16 builds with Turbopack, and the glob loader for the blocks renderer is a `webpack` rule | Replace the glob with the component map and remove the rule — [§ Replace the lookup](migrating-to-editable-regions.md#replace-the-lookup-with-a-component-map) |
SKILL.md
---
name: cloudcannon-bookshop
description: >-
Use when a CloudCannon site uses Bookshop — `*.bookshop.yml` files,
`@bookshop/*` packages, `_bookshop_name` keys, `bookshop:live`, or
`partial "bookshop"` calls or `{% bookshop %}` tags. Covers how Bookshop works, maintaining a site
that stays on it, and migrating it to editable regions.
---
# CloudCannon Bookshop
Bookshop is CloudCannon's earlier component system: a component library, a build step that generates the editor's structures, and a live-editing engine that re-renders components in the Visual Editor. [Editable regions](../cloudcannon-visual-editing/SKILL.md) replace it. This skill covers reading a Bookshop site, keeping one running, and moving it off Bookshop.
## When to use
- The site has any signal in [§ Detecting Bookshop](#detecting-bookshop)
- Adding or changing a component on a Bookshop site
- Migrating a Bookshop site to editable regions
- Bookshop live editing, structures or builds are misbehaving
## When not to use
- **The site has no Bookshop signals** — use [`cloudcannon-visual-editing`](../cloudcannon-visual-editing/SKILL.md). **MUST NOT** add Bookshop to a site that doesn't have it; new component editing is always editable regions.
- **Bookshop is gone and only regions remain** — that is [`cloudcannon-visual-editing`](../cloudcannon-visual-editing/SKILL.md), including its troubleshooting.
- **Configuration unrelated to components** — [`cloudcannon-configuration`](../cloudcannon-configuration/SKILL.md).
## Detecting Bookshop
Any one signal means the site uses Bookshop.
| Signal | Where |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| `bookshop/bookshop.config.{js,cjs}` | `src/` (Astro) or `component-library/` (Hugo, Eleventy, Jekyll, Next.js) |
| `*.bookshop.{yml,yaml,toml,json,js,cjs}` beside each component | `component-library/components/` or `src/components/` |
| `@bookshop/*` packages | `package.json`, in `dependencies` or `devDependencies` |
| `npx @bookshop/generate` | `.cloudcannon/postbuild` |
| `_bookshop_name` keys | Front matter, usually under `content_blocks`; sometimes component code too |
| The SSG's Bookshop plugin, module or integration, and its template calls | Site config and layouts — see the SSG's `bookshop.md` |
## SSG support
Migrate when editable regions can re-render the SSG's components. Otherwise the site stays on Bookshop.
| SSG | Bookshop engine | Path | Docs |
| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| Astro | `@bookshop/astro-engine` | Migrate — [migrating-to-editable-regions.md](migrating-to-editable-regions.md) | [astro/overview.md](astro/overview.md) |
| Hugo | `@bookshop/hugo-engine` | Migrate — [migrating-to-editable-regions.md](migrating-to-editable-regions.md) | [hugo/overview.md](hugo/overview.md) |
| Eleventy | `@bookshop/eleventy-engine` | Migrate — [migrating-to-editable-regions.md](migrating-to-editable-regions.md) | [eleventy/overview.md](eleventy/overview.md) |
| Next.js | None — `@bookshop/generate` writes structures only | Migrate — [migrating-to-editable-regions.md](migrating-to-editable-regions.md) | [nextjs/overview.md](nextjs/overview.md) |
| Jekyll | `@bookshop/jekyll-engine` | **Stay on Bookshop** — [maintaining.md](maintaining.md). Editable regions have no Jekyll component support yet | — |
## Maintain or migrate?
| The user wants… | Do |
| ---------------------------------------------------------- | ------------------------------------------------------------------------- |
| A component added or changed, nothing more | [maintaining.md](maintaining.md) — don't start a migration unasked |
| Off Bookshop, on an SSG marked Migrate | [migrating-to-editable-regions.md](migrating-to-editable-regions.md) |
| Off Bookshop, on an SSG marked Stay | Explain it isn't possible yet; offer [maintaining.md](maintaining.md) |
| The site moved onto CloudCannon (`migrate-to-cloudcannon`) | Migrate, inside that journey's phases — the migration file maps the steps |
| Unclear | Ask. A migration rewrites every component and page layout |
## Contents
| File | Covers |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| **SKILL.md** (this file) | Detection, SSG support, routing |
| [bookshop.md](bookshop.md) | How Bookshop works — library layout, `*.bookshop.yml`, `@bookshop/generate`, live editing, bindings |
| [maintaining.md](maintaining.md) | Changing a site that stays on Bookshop |
| [migrating-to-editable-regions.md](migrating-to-editable-regions.md) | The step order, structures from `*.bookshop.yml`, the `_bookshop_name` rename, removing Bookshop |
| [troubleshooting.md](troubleshooting.md) | Symptom → cause → fix, on Bookshop sites and partway through a migration |
**SSG-specific:**
Enter through the SSG's `overview.md`; it gives the reading order for that SSG's files.
| SSG | Doc | Purpose |
| -------- | -------------------------------------------- | --------------------------- |
| Astro | [astro/overview.md](astro/overview.md) | **Start here for Astro** |
| Hugo | [hugo/overview.md](hugo/overview.md) | **Start here for Hugo** |
| Eleventy | [eleventy/overview.md](eleventy/overview.md) | **Start here for Eleventy** |
| Next.js | [nextjs/overview.md](nextjs/overview.md) | **Start here for Next.js** |
**Other skills:**
| Skill | When to read |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| [cloudcannon-visual-editing](../cloudcannon-visual-editing/SKILL.md) | During a migration — the regions, the census and the completeness checklist |
| [cloudcannon-configuration](../cloudcannon-configuration/SKILL.md) | Writing and validating the `_structures` that replace generated ones |
| [cloudcannon-dev-server](../cloudcannon-dev-server/SKILL.md) | Letting the user try the migrated site in a local editor |
## Common mistakes
| Excuse | Reality |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| "It's a Bookshop site, so I'll migrate it" | Only if the user asked and the SSG is marked Migrate. A component change on a Bookshop site is a Bookshop change. |
| "This new component can be an editable-regions one" | On a site staying on Bookshop, new components are Bookshop components — [maintaining.md § Adding a component](maintaining.md#adding-a-component). |
troubleshooting.md
# Troubleshooting
Symptom → cause → fix. SSG-specific rows live in the SSG's `troubleshooting.md` — [Astro](astro/troubleshooting.md), [Hugo](hugo/troubleshooting.md), [Eleventy](eleventy/troubleshooting.md), [Next.js](nextjs/troubleshooting.md).
## On a Bookshop site
| Symptom | Cause | Fix |
| -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| The editor's Add menu for a component array is empty | `@bookshop/generate` didn't run or failed, so the structures were never written; or `spec.structures` names a different key than the input's `_structures.<key>` | Check the CloudCannon build log for the postbuild step. Align the two keys |
| A new component is missing from the Add menu | Its `spec.structures` doesn't list the array's structure key, or the site hasn't built on CloudCannon since it was added | Add the key; build on CloudCannon — structures only exist in CloudCannon builds |
| Build log: "Could not find any Bookshops" | No `bookshop/bookshop.config.{js,cjs}`, or it's inside a dot-directory | Add the config, or pass `--dot` to `@bookshop/generate` |
| Build log: "Could not find any output sites" | `@bookshop/generate` ran without a CloudCannon build output — usually run locally | It only runs on CloudCannon. Build locally with the SSG alone |
| Build log: "No live editing connected as no pages contained Bookshop components" | No built page carries Bookshop's HTML comments — components rendered outside Bookshop's calls, or comments minified away | Render through the SSG's Bookshop calls; keep HTML comments in minified output — see the SSG's troubleshooting |
| Build log: "…contains old (2.0) syntax" | A `*.bookshop.yml` still uses `props` / `component` | `npx @bookshop/up@latest` rewrites them |
| Build log: "referenced bookshop:x, but the component x does not exist" | A `bookshop:` slot in a blueprint names a missing component | Fix the name, or add the component |
| Editor console: "Your Bookshop SSG plugin is running version X, but @bookshop/live is running version Y" | The Bookshop packages are on different versions | [maintaining.md § Upgrading Bookshop](maintaining.md#upgrading-bookshop) |
| Editor banner: "Error Loading Bookshop Live Editing" | `/_cloudcannon/bookshop-live.js` failed to load or start | Check the console and network tab for that file and its errors |
| A component doesn't update as you type, or isn't clickable | Logic in the layout between front matter and the call | Move the logic into the component — [bookshop.md § Live editing](bookshop.md#live-editing) |
| Site data is empty in live renders | The data file has no `data_config` entry | Add it to `cloudcannon.config.yml` |
| A component that shouldn't re-render live (nav, footer) does | Live rendering is on for every top-level call by default | `live_render: false` on that call |
| Console: "Error rendering bookshop component X … expected in certain cases" | A render threw — often transient while a block is being added, removed or reordered | Ignore it if it clears; otherwise debug that component |
## Partway through a migration
| Symptom | Cause | Fix |
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| The build breaks, or blocks vanish, right after renaming `_bookshop_name` | The Bookshop dispatcher still selects on `_bookshop_name` | Rename, resolve and dispatch in one step — [migrating-to-editable-regions.md § Steps](migrating-to-editable-regions.md#steps) |
| The Add menu is empty after removing the postbuild hook | The structures were generated, never committed | [migrating-to-editable-regions.md § Writing `_structures`](migrating-to-editable-regions.md#writing-_structures-from-bookshopyml) |
| Newly added blocks start with different values than under Bookshop | Hand-written structures pre-fill arrays or nested slots that Bookshop started empty | Match Bookshop's rewrites — [bookshop.md § What `@bookshop/generate` does](bookshop.md#what-bookshopgenerate-does) |
| Components are no longer clickable in the editor | Bookshop's bindings are gone and no regions replaced them | Run the census and add regions — [cloudcannon-visual-editing](../cloudcannon-visual-editing/SKILL.md) |
| A text region shows an error card on a number | A blueprint value Bookshop rendered as a bare number | Quote it — [editable-regions.md § Value types per region](../cloudcannon-visual-editing/editable-regions.md#value-types-per-region) |
Once Bookshop is fully removed, region problems belong to [cloudcannon-visual-editing/troubleshooting.md](../cloudcannon-visual-editing/troubleshooting.md).