Skip to content

Latest commit

 

History

History
2323 lines (1833 loc) · 61.6 KB

sveltekit.adoc

File metadata and controls

2323 lines (1833 loc) · 61.6 KB

SvelteKit Bookshop Reference Guide

Concepts

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.

Prerequisites

  • Bookshop requires Node >= 16 installed on your machine.

Starting Point

💡
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.

Creating your Bookshop

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
component-library/bookshop/bookshop.config.cjs
module.exports = {
    engines: {
        "@bookshop/svelte-engine": {}
    }
}

We’ll cover creating components and shared files in Authoring New Components.

Connecting Your Bookshop to SvelteKit

To use Bookshop with SvelteKit, the primary dependency is the @bookshop/sveltekit-bookshop npm package.

command
# 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.

vite.config.js
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.

Using Components in SvelteKit

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:

index.svelte
...

<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.

Using Shared Bookshop Helpers in SvelteKit

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

Importing Bookshop Styles

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.

Authoring New Components

💡
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

Authoring Component Template Files

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:

components/button/button.svelte
<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:

components/hero/hero.svelte
<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" />

Authoring Component Styles

Bookshop doesn’t interfere with your existing style loaders, so no special actions are needed.

Authoring Component Bookshop Files

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.

Component Spec

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.

Component Blueprint

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.

Component Preview

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 Configuration

_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.

Blueprint Arrays

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.

Nesting Components

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.

Initializing Nested Components

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.

Supported File Formats

💡
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.

Providing Custom Component Thumbnails

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.

Using the Bookshop Component Browser

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.

Integrating the Component Browser With Your Site

Coming Soon — Bookshop SvelteKit does not yet support embedding the Bookshop browser in a website.

Hosting a Component Library

Bookshop SvelteKit does not yet support hosting the Bookshop browser on a SvelteKit website.

Connecting Bookshop to CloudCannon

ℹ️
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.

.cloudcannon/postbuild
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.

Data Bindings

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.

Live Editing Site Data and Collections

In other SSGs, Bookshop drives the live editing experience. For SvelteKit, this is (currently) outside the scope of Bookshop.

Passing Data to Bookshop Components

In other SSGs, Bookshop drives the live editing experience. For SvelteKit, this is (currently) outside the scope of Bookshop.

SvelteKit Live Editing Support

In other SSGs, Bookshop drives the live editing experience. For SvelteKit, this is (currently) outside the scope of Bookshop.

Rendering Different Content When Live Editing

In other SSGs, Bookshop drives the live editing experience. For SvelteKit, this is (currently) outside the scope of Bookshop.

Disabling Live Editing

In other SSGs, Bookshop drives the live editing experience. For SvelteKit, this is (currently) outside the scope of Bookshop.