Bookshop defines conventions for writing components for SvelteKit. Using these conventions, Bookshop provides an ergonomic way to build pages out of your components, build and browse these components locally, and generate rich live editing experiences in CloudCannon.
For an example of what this looks like in a real-world example, see our Editing MegaKit with Bookshop video.
💡
|
Short on time? You can use our SvelteKit Bookshop Starter Template and jump straight into editing it in CloudCannon. Come back here when you want to build it out further, or create your own from scratch. |
This guide will walk you through getting Bookshop connected to an existing SvelteKit site. If you don’t have a site already, reading SvelteKit’s Getting Started is a good start. Alternatively, grab one of CloudCannon’s preconfigured SvelteKit templates.
The first step is to create the directory structure for your Bookshop. To create this structure, you can run the following command in the root of your repository:
npx @bookshop/init --new component-library --framework svelte
This command should provide you with the following directory structure:
component-library/
├─ bookshop/
│ └─ bookshop.config.cjs
├─ components/
│ └─ sample/
│ ├─ sample.bookshop.yml
│ └─ sample.svelte
└─ shared/
└─ sveltekit
└─ page.svelte
Here’s a quick run-through of what has been generated:
- bookshop/bookshop.config.cjs
-
This houses the configuration for your Bookshop project, in this case instructing Bookshop to use the
@bookshop/svelte-engine
package for any live component rendering. - components/
-
This is where you will write your component files, a sample component has been added for you.
- shared/sveltekit/
-
Any non-component files that you want to be able to use when live editing can be placed here. A page helper has been created, which helps render arrays of components.
Creating these files yourself?
Bookshop File Reference
module.exports = {
engines: {
"@bookshop/svelte-engine": {}
}
}
We’ll cover creating components and shared files in Authoring New Components.
To use Bookshop with SvelteKit, the primary dependency is the @bookshop/sveltekit-bookshop
npm package.
# npm
npm i --save-exact @bookshop/sveltekit-bookshop
# or yarn
yarn add --exact @bookshop/sveltekit-bookshop
Within your Vite config, specify a $bookshop
alias with the path to your Bookshop project.
import { sveltekit } from '@sveltejs/kit/vite';
import { resolve } from 'path';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()],
resolve: {
alias: {
$bookshop: resolve('./component-library/')
}
},
server: {
fs: {
// Allow serving files from one level up to the project root
allow: ['..'],
},
},
};
export default config;
💡
|
Make sure that $bookshop points to the component library you just created, relative to your SvelteKit source.
|
💡
|
allow: ['..'] in the server.fs configuration allows Vite to serve the component files. This will need to be adusted to include your component library if it exists in another location.
|
Lastly, we’ll need to install a few npm packages for Bookshop. These aren’t used as part of your production build, but they provide the developer tooling that enables structured data and live editing.
These packages should be installed at the root of the repository that contains your site. If this folder doesn’t have a package.json
file yet, run npm init -y
to create one.
To get setup, run the following command to install the needed Bookshop packages:
# npm
npm i --save-exact @bookshop/generate @bookshop/browser @bookshop/svelte-engine
# or yarn
yarn add --exact @bookshop/generate @bookshop/browser @bookshop/svelte-engine
🌟
|
Bookshop uses a fixed versioning scheme, where all packages are released for every version. It is recommended that you keep the npm packages and your plugins at the same version. To help with this, you can run npx @bookshop/up@latest from your repository root to update all Bookshop packages in sync.
|
If you ran the @bookshop/init
command earlier, you should see that you now have a file at components/sample/sample.svelte
. Let’s have a go using that component somewhere on our site.
💡
|
We’ll cover creating these components soon — if you want to add a new component now, you can run npx @bookshop/init --component <name> in your Bookshop directory to scaffold it out automatically.
|
Bookshop provides a Bookshop
wrapper to render components. To start, add the following snippet to one of your pages:
...
<script>
import { Bookshop } from "@bookshop/sveltekit-bookshop";
</script>
<div>
<Bookshop component="sample" text="Hello from the sample component" />
</div>
...
If you now load your SvelteKit site in a browser, you should see the sample component rendered on the page.
💡
|
The Bookshop name of a component is the path to its directory. So the name for components/sample/sample.svelte is sample ,and the name for components/generic/button/button.svelte would be generic/button .
|
💡
|
The structures generated by Bookshop for CloudCannon include a _bookshop_name field for you, which can be used to render components dynamically. We’ll cover this a bit later on in Connecting Bookshop to CloudCannon.
|
Shared Bookshop helpers can be placed in the shared/sveltekit
directory. i.e:
component-library/
├─ components/
└─ shared/
└─ sveltekit/
└─ helper.svelte
This can then be included using Bookshop wrapper with the shared
prop:
<Bookshop shared="helper" lorem="ipsum" />
You will notice that @bookshop/init
created a page.svelte
file for you. Given the following front matter:
content_blocks:
- _bookshop_name: hero
hero_text: Hello World
image: /image.png
- _bookshop_name: cta
heading: Join our newsletter
location: /signup
You can render the array of components using the page helper like so:
<Bookshop shared="page" {content_blocks} />
This will loop through the given array, and render each component according to its _bookshop_name
key.
Give this a try now — replace the sample component you added with the page
helper, and add the following to your front matter:
content_blocks:
- _bookshop_name: sample
text: A sample example
- _bookshop_name: sample
text: A second sample example
In other SSGs Bookshop supports a <component>.scss
alongside each component.
Since Svelte supports styles inside component files, you can continue to use your existing setup for styles.
💡
|
To create new components, you can simply run npx @bookshop/init --component <name> in an existing Bookshop
|
Components live within the components/
directory, each inside a folder bearing their name. A component is defined with a <name>.bookshop.<format>
file. This file serves as the schema for the component, defining which properties it may be supplied.
Components may also be nested within folders, which are then referenced as part of the component name. For example, the following structure would define the components hero
, button/large
and button/small
:
components/
├─ hero/
| | hero.bookshop.yml
| └─ hero.svelte
└─ button/
├─ large/
| | large.bookshop.yml
│ └─ large.svelte
└─ small/
| small.bookshop.yml
└─ small.svelte
Beyond the naming convention, Bookshop template files are what you would expect when working with SvelteKit. A basic button component might look like the following:
<script>
export let link_url;
export let link_text;
</script>
<a class="c-button" href={ link_url }>{ link_text }</a>
Components can, of course, reference other components:
<script>
import { Bookshop } from "@bookshop/sveltekit-bookshop";
export let hero_text;
export let link_url;
</script>
<h1>{ hero_text }</h1>
<Bookshop component="button" {link_url} link_text="Click me" />
Bookshop doesn’t interfere with your existing style loaders, so no special actions are needed.
The Bookshop file for each component is the most important piece of the Bookshop ecosystem. This file drives the Structured Data in CloudCannon, the local component browser, and Bookshop’s live editing.
The sample.bookshop.yml
file that our init command generated contains the following:
sample.bookshop.yml
# Metadata about this component, to be used in the CMS
spec:
structures:
- content_blocks
label: Sample
description:
icon:
tags:
# Defines the structure of this component, as well as the default values
blueprint:
text: "Hello World!"
# Overrides any fields in the blueprint when viewing this component in the component browser
preview:
# Any extra CloudCannon inputs configuration to apply to the blueprint
_inputs: {}
Let’s walk through an example file section by section to understand what’s going on.
spec:
structures:
- content_blocks
label: Example
description: An example Bookshop component
icon: book
tags:
- example
This section is used when creating the Structure for your component. The structures
array defines which structure keys to register this component with. In other words, with the above snippet, this component will be one of the options within an array named content_blocks
, or another input configured to use _structures.content_blocks
.
The other keys are used when the component is displayed in CloudCannon or in the Bookshop Component Browser. icon
should be the name of a suitable material icon to use as the thumbnail for your component.
blueprint:
text: Hello World!
The blueprint is the primary section defining your component. This will be used as the intitial state for your component when it is added to a page, and should thus include all properties used in your template.
preview:
text: Vestibulum id ligula porta felis euismod semper.
Your blueprint represents the initial state of your component, but in the component browser you might want to see a preview of your component filled out with example data.
The preview object will be merged with your blueprint before a component is rendered in the component browser. This is a deep merge, so given the following specification:
blueprint:
hero_text: "Hello World"
cta:
button_text: ""
button_url: "#"
preview:
cta:
button_text: "Click me"
Your component preview data will be:
hero_text: "Hello World"
cta:
button_text: "Click me"
button_url: "#"
ℹ️
|
In a future Bookshop release, component thumbnails will be automatically generated. This will also use the preview object. |
_inputs:
text:
type: "html"
comment: "This comment will appear in the CMS"
The _inputs
section of your Bookshop file can be used to configure the keys in your blueprint. This object is passed through unaltered to CloudCannon, so see the CloudCannon Inputs Documentation to read more.
This configuration is scoped to the individual component, so you can configure the same key differently across components — even if the components are nested within one another.
Arrays of objects in your blueprint will be transformed into CloudCannon Structures automatically, and initialized as empty arrays. Using the following Blueprint:
blueprint:
text: Sample Text
items:
- item_content: Hello World
A new component added to the page will take the form:
text: Sample Text
items: []
Editors will then be able to add and remove objects to the items
array.
Your blueprint can reference other components and structures to create rich page builder experiences:
blueprint:
hero_text: Hello World
button: bookshop:button
In this example, the button
key will become an Object Structure containing the values specified in your button
component blueprint. If you desired an array of buttons, you could use the following:
blueprint:
hero_text: Hello World
buttons: [bookshop:button] # equivalent
buttons:
- bookshop:button # equivalent
If you’re creating a layout component, you likely want to support a set of components. For this, you can reference the keys we defined in spec.structures
as such:
blueprint:
section_label: My Section
# Make header a single component that can be selected from the content_blocks set
header: bookshop:structure:content_blocks
# Make inner_components an array that can contain components marked content_blocks
inner_components: [bookshop:structure:content_blocks]
To give a concrete example, say we have the following hero.bookshop.yml
file:
spec:
structures: [content_blocks]
blueprint:
hero_text: Hello World
cta_button: bookshop:button
column_components: [bookshop:structure:content_blocks]
Then our hero.svelte
file to render this might look like the following:
<script>
import { Bookshop } from "@bookshop/sveltekit-bookshop";
export let hero_text;
export let cta_button;
export let column_components;
</script>
<div class="hero">
<h1>{ hero_text }</h1>
{#if cta_button}
<Bookshop component="button" {...cta_button} />
{/if}
{#each column_components as component}
<Bookshop {...component} />
{/each}
</div>
🌟
|
Object Structures in CloudCannon may be empty, so testing for the existence of this component in your template is recommended. |
By default, nested components using the bookshop:
shorthand will be initialized empty. For example, the blueprint:
blueprint:
hero_text: Hello World
button: bookshop:button
Will be initialized in CloudCannon as:
hero_text: Hello World
button:
Where button
will provide an editor with the option to add a button component. To instead have the button component exist on creation, you can use the syntax bookshop:button!
:
blueprint:
hero_text: Hello World
button: bookshop:button!
The same setting can be applied to a structure shorthand by specifying the component that should be initialized. Taking the following example:
blueprint:
hero_text: Hello World
column_components:
- bookshop:structure:content_blocks!(hero)
- bookshop:structure:content_blocks!(button)
This will be initialized in CloudCannon as:
hero_text: Hello World
column_components:
- _bookshop_name: hero
# hero fields
- _bookshop_name: button
# button fields
Where column_components
can be then further added to/removed from by an editor, as per the tagged structure.
💡
|
When you run npx @bookshop/init --component <name> you will be prompted to pick which configuration format you want to create the component with.
|
In the examples above, we have been writing the Bookshop configuration files using YAML. This is the recommended format, but you can also choose another if you prefer. Here is a real-world example of a component written in each supported format:
hero.bookshop.yml
# Metadata about this component, to be used in the CMS
spec:
structures:
- content_blocks
- page_sections
label: Hero
description: A large hero component suitable for opening a landing page
icon: crop_landscape
tags:
- Above the Fold
- Multimedia
# Defines the structure of this component, as well as the default values
blueprint:
hero_text: ""
hero_level: h1
hero_image: ""
hero_image_alt: ""
subcomponents: [bookshop:structure:content_blocks]
# Overrides any fields in the blueprint when viewing this component in the component browser
preview:
hero_text: Bookshop Makes Component Driven Development Easy
hero_image: https://placekitten.com/600/400
# Any extra CloudCannon inputs configuration to apply to the blueprint
_inputs:
hero_level:
type: select
options:
values:
- h1
- h2
- h3
- h4
hero.bookshop.toml
# Metadata about this component, to be used in the CMS
[spec]
structures = [ "content_blocks", "page_sections" ]
label = "Hero"
description = "A large hero component suitable for opening a landing page"
icon = "crop_landscape"
tags = [ "Above the Fold", "Multimedia" ]
# Defines the structure of this component, as well as the default values
[blueprint]
hero_text = ""
hero_level = "h1"
hero_image = ""
hero_image_alt = ""
subcomponents = [ "bookshop:structure:content_blocks" ]
# Overrides any fields in the blueprint when viewing this component in the component browser
[preview]
hero_text = "Bookshop Makes Component Driven Development Easy"
hero_image = "https://placekitten.com/600/400"
# Any extra CloudCannon inputs configuration to apply to the blueprint
[_inputs]
hero_level.type = "select"
hero_level.options.values = [ "h1", "h2", "h3", "h4" ]
hero.bookshop.js
module.exports = () => {
const spec = {
structures: [
"content_blocks",
"page_sections",
],
label: "Hero",
description: "A large hero component suitable for opening a landing page",
icon: "crop_landscape",
tags: [
"Above the Fold",
"Multimedia",
]
};
const blueprint = {
hero_text: "",
hero_level: "h1",
hero_image: "",
hero_image_alt: "",
subcomponents: [ "bookshop:structure:content_blocks" ],
};
const preview = {
hero_text: "Bookshop Makes Component Driven Development Easy",
hero_image: "https://placekitten.com/600/400",
};
const _inputs = {
hero_level: {
type: "select",
options: {
values: [
"h1",
"h2",
"h3",
"h4",
]
}
}
};
return {
spec,
blueprint,
preview,
_inputs,
}
}
hero.bookshop.json
{
"spec": {
"structures": [
"content_blocks",
"page_sections"
],
"label": "Hero",
"description": "A large hero component suitable for opening a landing page",
"icon": "crop_landscape",
"tags": [
"Above the Fold",
"Multimedia"
]
},
"blueprint": {
"hero_text": "",
"hero_level": "h1",
"hero_image": "",
"hero_image_alt": "",
"subcomponents": [ "bookshop:structure:content_blocks" ]
},
"preview": {
"hero_text": "Bookshop Makes Component Driven Development Easy",
"hero_image": "https://placekitten.com/600/400"
},
"_inputs": {
"hero_level": {
"type": "select",
"options": {
"values": [
"h1",
"h2",
"h3",
"h4"
]
}
}
}
}
💡
|
Can’t decide? You can always run npx @bookshop/up --format <format> to automatically convert all of your files if you change your mind.
|
When an editor is selecting a component in CloudCannon, the icon
from the component spec will be used as the thumbnail. You can provide a custom image to use instead by placing a <component>.preview.<format>
in your component directory. To provide a custom icon, which will be shown when viewing an array of components, you can also provide a <component>.icon.<format>
file.
components/
└─ hero/
| hero.bookshop.yml
├─ hero.preview.png
├─ hero.icon.svg
└─ hero.svelte
See the CloudCannon Structures Reference for extra keys that you can set in your component spec to control the display of these images.
The Bookshop component browser allows you to browse and experiment with your components. When running in development the component browser also provides hot reloading of component templating and styles. An example browser showing the components in our Eleventy starter template can be seen here: https://winged-cat.cloudvent.net/components/
In your local development environment, run:
npx @bookshop/browser
By default, this will discover any Bookshop directories in or under the current working directory, and will host a component library on port 30775.
After running this command, a component browser will be viewable on http://localhost:30775
💡
|
Run npx @bookshop/browser --help to see the available options.
|
Coming Soon — Bookshop SvelteKit does not yet support embedding the Bookshop browser in a website.
ℹ️
|
This guide assumes that your site is already set up with CloudCannon. If this isn’t the case, hop over to the CloudCannon Documentation and get setup with a successful build first. |
Now that you understand how everything works locally, we can integrate Bookshop with CloudCannon. Bookshop does most of the heavy lifting for you, so we’ll get to see the benefits pretty quickly.
The main thing you need to do is create a postbuild script that runs Bookshop’s generate script. This should be placed inside a folder named .cloudcannon
at the root of your repository.
npm i
npx @bookshop/generate
This command will automatically discover your component library as well as the output site from your build, and will then generate CloudCannon Structures for your components.
Bookshop does not handle live editing for SvelteKit websites, as this is supported natively with CloudCannon and SvelteKit. See Live editing with Svelte on the CloudCannon documentation.
If you’re using the CloudCannon Svelte live rendering, Bookshop can automatically create Visual Data Bindings for your components. To do so, you need to import trackBookshopLiveData
and wrap the object that is provided from onCloudCannonChanges
:
<script>
import { onDestroy, onMount } from "svelte";
import {
onCloudCannonChanges,
stopCloudCannonChanges,
} from "@cloudcannon/svelte-connector";
import {
Bookshop,
trackBookshopLiveData,
} from "@bookshop/sveltekit-bookshop";
export let pageDetails;
onMount(async () => {
onCloudCannonChanges(
(newProps) => (pageDetails = trackBookshopLiveData(newProps))
);
});
onDestroy(async () => {
stopCloudCannonChanges();
});
</script>
<div>
<Bookshop shared="page" content_blocks={pageDetails.content_blocks} />
</div>
If a component is passed data from the page front matter, you will be able to interact with that component directly on the page.
By default, Bookshop will add bindings for any components on the page, but will not add bindings for shared helper files. This prevents Bookshop rendering data bindings around our shared page
helper, so that the components within are immediately accessible.
This behavior can be customised by including a flag in the component’s data. Bookshop will look for any of the following keys:
-
data_binding
-
dataBinding
-
_data_binding
-
_dataBinding
For example:
<!-- This component will **not** get a binding -->
<Bookshop component="item" dataBinding=false {...props} />
<!-- This include **will** get a binding -->
<Bookshop shared="page" dataBinding=true {...props} />
ℹ️
|
This flag only applies to the component directly and doesn’t cascade down. Any subcomponents will follow the standard rules, or can also specify their own Data Binding flag. |
In other SSGs, Bookshop drives the live editing experience. For SvelteKit, this is (currently) outside the scope of Bookshop.
In other SSGs, Bookshop drives the live editing experience. For SvelteKit, this is (currently) outside the scope of Bookshop.
In other SSGs, Bookshop drives the live editing experience. For SvelteKit, this is (currently) outside the scope of Bookshop.
In other SSGs, Bookshop drives the live editing experience. For SvelteKit, this is (currently) outside the scope of Bookshop.