Skip to main content

Add a headless CMS to Preact in 5 minutes

Contents
    Try Storyblok

    Storyblok is the first headless CMS that works for developers & marketers alike.

    This short tutorial will look at integrating Storyblok into a Preact application. We will learn how we get the data from Storyblok and how we enable Storyblok Bridge to preview live changes in the Visual Editor.

    HINT:

    You can find the final code for this tutorial in this repository.

    Requirements

    Here are a few requirements to follow this tutorial:

    • Understanding of Preact and Javascript.

    • Node.js LTS version (npm or yarn installed).

    • A Storyblok App account for creating a project in Storyblok.

    Project Setup

    Let’s start by creating a new Preact Project. We are going to use Vite for setting up our development environment.

    # npm 6.x
    npm create vite@latest my-preact-app --template preact
    # npm 7+, extra double-dash is needed:
    npm create vite@latest my-preact-app -- --template preact
    # yarn
    yarn create vite my-preact-app --template preact

    Now we also need to install a few more packages. First, vite-plugin-mkcert will make our development server run on HTTPS. This is required for using Storyblok V2. We also need to install @storyblok/js, Storyblok's official JavaScript SDK.

    cd my-preact-app
    # npm
    npm install vite-plugin-mkcert @storyblok/js
    # yarn
    yarn add vite-plugin-mkcert @storyblok/js

    Once the above packages are installed, we need to update our vite.config.js file to run our dev server on HTTPS.

    import { defineConfig } from "vite";
    import preact from "@preact/preset-vite";
    import mkcert from "vite-plugin-mkcert";
    
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [preact(), mkcert()],
    });

    Once our vite.config.js file looks like the above we can start our development server.

    npm run dev
    # yarn dev

    It should automatically open a tab in the browser with the URL https://localhost:5173/, or we can manually go to the URL after the project starts running. You should see this screen:

    https://localhost:5173
    Fresh Vite + Preact Project

    Fresh Vite + Preact Project

    Space Configuration

    Now create a new space in the Storyblok App by clicking "Create New". Select the first option to start from scratch {1} and give it a name {2}. Then we can hit "Create space" {3}.

    app.storyblok.com
    Creating a new space in Storyblok
    1
    2
    3

    Creating a new space in Storyblok

    Every Storyblok space has some sample content and components. However, we need to configure our Storybook space so the visual editor gets the live preview of our frontend Preact App. For this, go to "Settings" {1}, "Visual Editor" {2}, set the "Location (default environment)" {3} to https://localhost:5173/, and finally, hit the "Save" button {4}.

    app.storyblok.com
    Setting the default environment in the Storyblok Settings
    1
    2
    3
    4

    Setting the default environment in the Storyblok Settings

    Now let’s go to the Home Page from the Content section. Click on the "Content" {1} and "Home Page" {2}. Once the page opens, we can see our Preact App preview.

    app.storyblok.com
    Navigating to the home page
    1
    2

    Navigating to the home page

    On the right-hand side, we can see two blocks Storyblok provides. Shortly we will learn how we can create Preact components that will visually represent these two blocks. But before we do that we need to update the Real Path of this story. Click on the "Entry Configuration" {1} and set the "Real Path" {2} to / . Finally, press "Save & Close" {3}.

    app.storyblok.com
    Update home page entry configuration
    1
    2
    3

    Update home page entry configuration

    Connect Preact to Storyblok

    Before we start, let's create a helper function that will make our life much easier.

    src/storyblok/storyblokHelper.js
    import { useState, useEffect } from "preact/hooks";
    import { registerStoryblokBridge as registerSbBridge } from "@storyblok/js";
    import { storyblokInit as sbInit } from "@storyblok/js";
    export { default as StoryblokComponent } from "./StoryblokComponent";
    export { storyblokEditable, useStoryblokBridge } from "@storyblok/js";
    export { apiPlugin, registerStoryblokBridge } from "@storyblok/js";
    export { renderRichText } from "@storyblok/js";
    
    let storyblokApiInstance = null;
    let componentsMap = {};
    
    export const useStoryblok = (slug, apiOptions = {}, bridgeOptions = {}) => {
      let [story, setStory] = useState({});
    
      if (!storyblokApiInstance) {
        console.error(
          "You can't use useStoryblok if you're not loading apiPlugin."
        );
    
        return null;
      }
    
      registerSbBridge(story.id, (story) => setStory(story), bridgeOptions);
    
      useEffect(() => {
        async function fetchData() {
          const { data } = await storyblokApiInstance.get(
            `cdn/stories/${slug}`,
            apiOptions
          );
          setStory(data.story);
        }
        fetchData();
      }, [slug]);
    
      return story;
    };
    
    export const useStoryblokState = (
      initialStory = {},
      bridgeOptions = {},
      preview = true
    ) => {
      let [story, setStory] = useState(initialStory);
      if (!preview) {
        return initialStory;
      }
      useEffect(() => {
        registerSbBridge(story.id, (newStory) => setStory(newStory), bridgeOptions);
        setStory(initialStory);
      }, [initialStory]);
      return story;
    };
    
    export const useStoryblokApi = () => {
      if (!storyblokApiInstance) {
        console.error(
          "You can't use getStoryblokApi if you're not loading apiPlugin."
        );
      }
    
      return storyblokApiInstance;
    };
    
    export { useStoryblokApi as getStoryblokApi };
    
    export const getComponent = (componentKey) => {
      if (!componentsMap[componentKey]) {
        console.error(`Component ${componentKey} doesn't exist.`);
        return false;
      }
      return componentsMap[componentKey];
    };
    
    export const storyblokInit = (pluginOptions = {}) => {
      const { storyblokApi } = sbInit(pluginOptions);
      storyblokApiInstance = storyblokApi;
      componentsMap = pluginOptions.components;
    };

    src/storyblok/StoryblokComponent.jsx
    import { getComponent } from "./storyblokHelper";
    
    const StoryblokComponent = ({ blok, ...restProps }) => {
      if (!blok) {
        console.error("Please provide a 'blok' property to the StoryblokComponent");
        return <div>Please provide a blok property to the StoryblokComponent</div>;
      }
      const Component = getComponent(blok.component);
      if (Component) {
        return <Component blok={blok} {...restProps} />;
      }
      return <div></div>;
    };
    export default StoryblokComponent;

    We created multiple helper functions and re-exported a few functions from @storyblok/js in the above two files. Let's quickly take a look at few important functions and what they do.

    Name Description
    storyblokInit This function will be used at the top label of our application to connect with storyblok. We will define our access token and all the components here.
    storyblokEditable This function will help make our components editable in Storyblok visual editor.
    StoryblokComponent This function will map all of our Storyblok components with Preact component.
    useStoryblok This function will help us to get the content based on the current slug.

    Let's begin the fun part

    Connect the Preact Application to Storyblok with the help of the above helper function. We are going to use two things from the above in the main.jsx file storyblokInit and apiPlugin. We need to add the following code to the main.jsx file.

    src/main.jsx
    ...
    
    import { storyblokInit, apiPlugin } from "./storyblok/storyblokHelper";
    storyblokInit({
      accessToken: "YOUR_PREVIEW_TOKEN",
      use: [apiPlugin],
      apiOptions: { https: true },
      components: {},
    });
    
    ...

    storyblokInit will allow us to set up the connection with the space and load the Storyblok Bridge, which helps us see real-time changes when editing the content in Storyblok. The apiPlugin helps us retrieve the data.

    The storyblokInit function also has a component parameter. Here, we have to declare all the Preact components according to the ones we have in our space. These components are dynamically rendered with the StoryblokComponent which we will see shortly.

    HINT:

    It’s a common pattern to name the Preact components the same as in our Storyblok space.

    Before we create our components we also need to get the preview token and place the value in accessToken field. To do this, go to "Settings" {1}, "Access Tokens" {2}, and copy the "Preview" access token {3}.

    app.storyblok.com
    Getting the preview access token for our project
    1
    2
    3

    Getting the preview access token for our project

    Now that we have the preview access key, we can load the data dynamically based on the current page. Let's make a few updates in our App.jsx file.

    src/App.jsx
    import { useStoryblok, StoryblokComponent } from "./storyblok/storyblokHelper";
    function App() {
      let slug =
        window.location.pathname === "/"
          ? "home"
          : window.location.pathname.replace("/", "");
      const story = useStoryblok(slug, { version: "draft" });
      if (!story || !story.content) {
        return <div>Loading...</div>;
      }
      return <StoryblokComponent blok={story.content} />;
    }
    export default App;

    In order to load dynamic data based on the page we need to get the current slug. After we get the slug we are using useStoryblok from our helper function. This useStoryblok takes a slug:string as the first and apiOptions:object as the second parameter. It can also take a third and last optional parameter bridgeOptions:object.

    useStoryblok Params
    Parameter Description
    slug* The first parameter of type string. Slug of the required story
    apiOptions* The second parameter of type object, for configuring the API options.
    bridgeOptions This is an optional parameter of type object, for customizing the bridge options.

    We also see StoryblokComponent in action here. We can pass the content for a story with blok props. And it's going to map the Preact components we created according to our space and listed in storyblokInit.

    In Storyblok, all the content is structured as components. As we already have some components created in our space, let’s create those in our Preact app. This will allows us to reuse the components dynamically.

    Creating Components

    When we create a new space, the default components are: Page, Teaser, Grid and Feature. Now let's create these components in our app.

    In the components folder:

    src/components/Page.jsx
    import { StoryblokComponent, storyblokEditable } from "../storyblok/storyblokHelper";
    const Page = ({ blok }) => (
      <main {...storyblokEditable(blok)}>
        {blok.body
          ? blok.body.map((blok) => (
              <StoryblokComponent blok={blok} key={blok._uid} />
            ))
          : null}
      </main>
    );
    export default Page;

    In the Page component, we are using storyblokEditable function from our helper function. It will allow us to mark the preact component as editable in the Storyblok Visual Editor. With the help of this function, we can click the component in the Visual Editor and easily edit them. Hence we will use this for all the Storyblok components.

    src/components/Teaser.jsx
    import { storyblokEditable } from "../storyblok/storyblokHelper";
    const Teaser = ({ blok }) => {
      return (
        <h2 style={{ textAlign: "center" }} {...storyblokEditable(blok)}>
          {blok.headline}
        </h2>
      );
    };
    export default Teaser;

    src/components/Grid.jsx
    import { StoryblokComponent, storyblokEditable } from "../storyblok/storyblokHelper";
    const Grid = ({ blok }) => {
      return (
        <div
          style={{ display: "flex", justifyContent: "space-around" }}
          {...storyblokEditable(blok)}
          className="grid"
        >
          {blok.columns.map((blok) => (
            <StoryblokComponent blok={blok} key={blok._uid} />
          ))}
        </div>
      );
    };
    export default Grid;

    src/components/Feature.jsx
    import { storyblokEditable } from "../storyblok/storyblokHelper";
    const Feature = ({ blok }) => (
      <div {...storyblokEditable(blok)} className="column feature">
        {blok.name}
      </div>
    );
    export default Feature;

    Now we have all the Preact components the same as in our Storyblok space we need to add these in storyblokInit in main.jsx.

    src/main.jsx
    import { render } from "preact";
    import App from "./app";
    import "./index.css";
    import Page from "./components/Page";
    import Grid from "./components/Grid";
    import Feature from "./components/Feature";
    import Teaser from "./components/Teaser";
    import { storyblokInit, apiPlugin } from "./storyblok/storyblokHelper";
    const components ={
      page: Page,
      teaser: Teaser,
      feature: Feature,
      grid: Grid,
    }
    storyblokInit({
      accessToken: "YOUR_PREVIEW_TOKEN",
      use: [apiPlugin],
      components,
    });
    render(<App />, document.getElementById("app"));

    And that’s all! We should be able to see our content in the Visual Editor, now that we’ve unlocked the power of live editing. We can start playing with the content and see live changes. It should look something like this:

    app.storyblok.com
    Live editing in storyblok

    Live editing in storyblok

    Wrapping Up

    In this tutorial, we saw an overview of creating and integrating a Preact Application with Storyblok. Additionally, we learned how to use the data and enable the real-time Visual Editor.