Add a headless CMS to Remix in 5 minutes

Contents
    Try Storyblok

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

    In this article, we’ll see how to integrate Storyblok into our Remix application, so we can manage the content of our project inside a Headless CMS. We'll also see how we can enable a live preview of the content that's created, using Storyblok's Visual Editor.

    HINT:

    If you have less than 5 minutes, you can take a look at the source code of the sample project on this GitHub Repository.

    Environment Setup

    In order to follow this guide, we assume that you meet these requirements:

    • Basic understanding of React and JavaScript.

    • Node.js LTS version.

    • Storyblok account, to create a space for the project.

    Create a new Remix project

    In the terminal, let's type the following command:

    npx create-remix@latest

    When prompted about the type of application we will build, choose "Just the basics" and then "Remix App Server". Also, we will use JavaScript for this tutorial, but feel free to choose TypeScript if you prefer.

    Executing this command will generate a boilerplate that we can use as the starting point for our new Remix application. After all the files are generated and the project is set up, we should be able to start the Remix application:

    cd [directory-of-your-project]
    npm run dev

    That initializes the development server, and we should be able to see the sample page of the project visiting http://localhost:3000.

    localhost:3000
    Remix starter page

    Remix starter page

    For this tutorial, we will set up our dev server with an HTTPS proxy, to use a secure connection with the application. We'll use port 3010, so the URL to access our website will end up being https://localhost:3010/.

    HINT:

    If you don’t know how to setup an HTTPS proxy on macOS, you can read this guide.

    Configure Storyblok space

    Let's create a new space for the project, so we can store and manage the content in it. Go to the Storyblok application, select "New Space" and click on "Create space". We should pick a name for it. This action should create a basic space with sample content.

    app.storyblok.com
    Create a new space

    Create a new space

    After we create our space, we need to configure the preview URL that will connect Storyblok to the frontend of our application. By doing this, we'll be able to see our website inside the Visual Editor. Once we're inside our new space, go to Settings {1} > Visual Editor {2}, and set the Location (default environment) to https://localhost:3010/.

    app.storyblok.com
    Preview URL
    1
    2

    Preview URL

    We can set our Home page as the initial page of our website. Go to Content and open the Home story in the Visual Editor. Click on the Entry configuration option, and set the Real Path to /.

    localhost:3010
    Set the real path
    1

    Set the real path

    Connect the Remix project to Storyblok

    We have a tool that will make it easy to connect our Remix application to Storyblok. Let's install the official Storyblok React SDK:

    npm install @storyblok/react

    This NPM package allows us to interact with the Storyblok API, and will help us to enable the real-time editing experience inside the Visual Editor. In order to do that, we need to add some configuration.

    HINT:

    If you want to learn more about configuration and options from @storyblok/react , you can read more from here. If you want to try out a demo, you can see our Stackblitz demo.

    In our Storyblok space, go to Settings {1} > Access Tokens {2}, and copy the Preview access token.

    app.storyblok.com
    Preview Access Token
    1
    2

    Preview Access Token

    Now, go to the app/root.jsx file in our Remix project, and add this piece of code before the existing functions, replacing the value of accessToken with the one we copied from the Storyblok space.

    app/root.jsx
    ...
    
    import { storyblokInit, apiPlugin } from "@storyblok/react";
    
    storyblokInit({
      accessToken: "your-preview-token",
      use: [apiPlugin],
    });
    
    ...
    HINT:

    If you’d like to learn more about environment variable configuration with Remix, we recommend you to have a look at this Remix documentation.

    storyblokInit sets up the connection with the space. It initializes the Storyblok Bridge, that allows us to enable the real-time editing experience inside the Visual Editor. The function also provides an instance of the Storyblok API client that we can use to retrieve content from Storyblok.

    As you may have already seen, the content in Storyblok is structured as components (or blocks). We can have stories/pages, that are composed of different components. You can see an example in the Home story of our new space.

    HINT:

    If you want to learn more about how the content is structured in Storyblok, you can read this guide.

    In order to display the content in our Remix application, to structure it, and to be able to manipulate it inside the Visual Editor, we need to create the visual representation of the components.

    Create and configure components

    We will create React components that will link to the component structures available in our Storyblok space. When a new space is created in Storyblok, it comes with four default component types: Page, Teaser, Grid, and Feature. Let's create React components for each one of them.

    Inside the app folder of our Remix project, create a components directory. Let's create one file for each React component:

    app/components/Page.jsx
    import { storyblokEditable, StoryblokComponent } from "@storyblok/react";
    
    const Page = ({ blok }) => (
      <main {...storyblokEditable(blok)} key={blok._uid}>
        {blok.body.map((nestedBlok) => (
          <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
        ))}
      </main>
    );
    
    export default Page;

    Here we are rendering the HTML markup for the Page content type. We can use all the properties coming from the Storyblok API to display the content. In this case, the Page content type has a property called body, which includes a list of blocks. 

    In this first component, we can identify two elements related to Storyblok, included in the @storyblok/react package:

    • storyblokEditable: This function allows us to mark a React component as "editable". So, when we load a page that contains it inside the Visual Editor, we'll be able to click on it and edit its properties. We'll mark all the React components that link to Storyblok components/blocks as "editable".

    • StoryblokComponent: This React generic component allows us to render any React component linked to a Storyblok item. So, in the case that we don't know the exact structure of a component, or which blocks are included in a certain property/structure, we can use this wildcard to render the content. To make StoryblokComponent work properly, we will need to list all the React components that we want to handle, and link them to their representation in the Storyblok space. We will see later how to do that.

    Let's create the other three components:

    app/components/Teaser.jsx
    import { storyblokEditable } from "@storyblok/react";
    
    const Teaser = ({ blok }) => {
      return (
        <div {...storyblokEditable(blok)} key={blok._uid}>
          <h2> {blok.headline} </h2>
        </div>
      );
    };
    
    export default Teaser;
    app/components/Grid.jsx
    import { storyblokEditable, StoryblokComponent } from "@storyblok/react";
    
    const Grid = ({ blok }) => (
      <ul {...storyblokEditable(blok)} key={blok._uid}>
        {blok.columns.map((blok) => (
          <li key={blok._uid}>
            <StoryblokComponent blok={blok} />
          </li>
        ))}
      </ul>
    );
    
    export default Grid;
    app/components/Feature.jsx
    import { storyblokEditable } from "@storyblok/react";
    
    const Feature = ({ blok }) => {
      return (
        <div {...storyblokEditable(blok)} key={blok._uid}>
          <h2> {blok.name} </h2>
        </div>
      );
    };
    
    export default Feature;

    As mentioned before, we need to configure the components identified by StoryblokComponent, and link them to their representation in the Storyblok space. To do that, let's go back to app/root.jsx and add a new parameter to storyblokInit call:

    app/root.jsx
    ...
    
    import { storyblokInit, apiPlugin } from "@storyblok/react";
    import Feature from "./components/Feature";
    import Grid from "./components/Grid";
    import Page from "./components/Page";
    import Teaser from "./components/Teaser";
    
    const components = {
      feature: Feature,
      grid: Grid,
      teaser: Teaser,
      page: Page,
    };
    storyblokInit({
      accessToken: "your-preview-token",
      use: [apiPlugin],
      components,
    });
    
    ...

    One more thing is left to finish our sample project: We need to create the Remix routes that will render the different pages of our website.

    Create the dynamic route

    If we want to handle multiple routes with a unique file definition, Remix offers Dynamic Routes. Let's create a file inside the app/routes directory called $slug.jsx. With it, we will handle all the different URLs that we'll have on our website, and all the slugs for the stories in our Storyblok space.

    app/routes/$slug.jsx
    import { json } from "@remix-run/node";
    import { useLoaderData } from "@remix-run/react";
    
    import {
      getStoryblokApi,
      useStoryblokState,
      StoryblokComponent,
    } from "@storyblok/react";
    
    export const loader = async ({ params }) => {
      const slug = params.slug ?? "home";
    
      let sbParams = {
        version: "draft",
      };
    
      let { data } = await getStoryblokApi().get(`cdn/stories/${slug}`, sbParams);
    
      return json(data?.story);
    };
    
    export default function Page() {
      let story = useLoaderData();
    
      story = useStoryblokState(story);
    
      return <StoryblokComponent blok={story.content} />;
    }

    In this dynamic route, we are executing code on the server (the loader function), that retrieves content from the Storyblok API, and a Page function that render HTML markup based on the API response.

    The loader function grabs the slug from the URL that is being requested, and uses it to bring the data related to the story from the Storyblok space that has the same slug. To do this, we use getStoryblokApi to use the instance of the API client that we initialized with storyblokInit

    The Page function consumes the retrieved story (useLoaderData) and sends it as a parameter for useStoryblokState. This function will link the story to the Storyblok Visual Editor, so we are able to load and manipulate the content inside of it. Now, we will be able to listen for Storyblok Visual Editor changes and update the state of the components in our page.

    Finally, as we don't know exactly which content type or components we will render on each page, we use StoryblokComponent with the content retrieved from the Storyblok API.

    This dynamic route would be able to cover all the URLs from entries without a nested folder structure. But there is a small issue: Remix, by definition, needs an index.jsx file inside the routes directory. The problem is that this will collide with the route of our home page. So, as we don't want to repeat the code from our dynamic route, we can replace the code of the index.jsx file to look like this:

    app/routes/index.jsx
    export { default, loader } from "./$slug";

    And that's it! We have our project ready to be used. Now, we can start the development server, go to the Storyblok Visual Editor and interact in real time with the different components of our pages.

    app.storyblok.com
    Storyblok real-time Visual Editor
    1
    2

    Storyblok real-time Visual Editor

    Create splat routes

    If you want to cover a folder nested entry structure, Remix already provides the approach to handle such cases. Splat routes from Remix can catch all slugs regardless of being nested by folders or without. In this case, instead of creating $slug.jsx, we create $.jsx file. By using splat routes, your content editors would be able to create new pages with a folder nested structure.

    app/routes/$.jsx
    import { json } from "@remix-run/node";
    import { useLoaderData } from "@remix-run/react";
    
    import {
      getStoryblokApi,
      useStoryblokState,
      StoryblokComponent,
    } from "@storyblok/react";
    
    export default function Page() {
      let story = useLoaderData();
      story = useStoryblokState(story);
    
      return (
        <>
          <StoryblokComponent blok={story.content} />
        </>
      )
    };
    
    export const loader = async ({ params, preview = false }) => {
      let slug = params["*"] ?? "home";
      let blogSlug = params["*"] === "blog/" ? "blog/home" : null;
    
      let sbParams = {
        version: "draft"
      };
    
      if (preview) {
        sbParams.version = "draft"
        sbParams.cv = Date.now()
      };
    
      let { data } = await getStoryblokApi().get(`cdn/stories/${blogSlug ? blogSlug : slug}`, sbParams);
      return json(data?.story, preview);
    };
    HINT:

    There is a Remix Conf talk YouTube video from us talking about how you can handle splat routes with Storyblok. You can see how editors can create nested routes from the Storyblok UI.

    Wrapping up

    In this tutorial, we saw how to create a web application using the Remix framework, and how to connect that project to a Storyblok space, in order to manage the content using a Headless approach. We also saw how to configure Storyblok's real-time Visual Editor, so we are able to see how the content that we create is going to look like when we publish it.

    Resources Link
    GitHub demo repository https://github.com/storyblok/storyblok-remix-boilerplate
    Remix docs https://remix.run/docs
    Storyblok Visual Editor https://www.storyblok.com/docs/editor-guides/visual-editor
    Storyblok React SDK https://github.com/storyblok/storyblok-react
    Storyblok Technologies Hub https://www.storyblok.com/technologies
    Remix Conf talk, "Remix your UI & UX to another level" https://www.youtube.com/watch?v=bUlIRAfxcM8