How to connect Eleventy and Storyblok
Tags: webdev, storyblok, eleventy, javascript
Published: 2024-02-10 21:49:09

_This is the second post in a series of articles called “Eleventy and Storyblok” to learn how to use this combination to create a web project._


Before getting into the matter, I really recommend you to read my first article of this series if you haven’t yet. Take a moment to check it out and discover why Evleventy and Storyblok is my perfect combination and why you should start using it to take advantage of these two technologies. You can read i <a href="https://estelafranco.com/blog/eleventy-storyblok/" target="_blank"> here</a>.

So, in this second article, I explain to you how to connect Eleventy and Storyblok to start consuming content generated in this amazing Headless CMS.

## Technical requirements
This article will show you step by step how to query data from Storyblok to use it in an Eleventy project. You'll need a computer with the following features:

- Node.js. Eleventy requires Node.js 14. I've used v20.10.0 in the examples provided. 
  - You can check if you have Node installed by running `node --version` in a terminal window.
  - If the command is not found or returns a number less than 14, you will need to download and install <a href="https://nodejs.org/en/download/" target="_blank" rel="nofollow noopener noreferrer">Node.js</a> before proceeding to the next step.
- A code editor. I use <a href="https://code.visualstudio.com/download" target="_blank" rel="nofollow noopener noreferrer">VSCode</a>, but feel free to use your favorite.
- A Storyblok account. You can create yours <a href="https://get.storyblok.com/estela-franco-cuenca" target="_blank" rel="nofollow noopener noreferrer">here</a>.
- Basic knowledge of HTML, CSS, JavaScript, Markdown, and the command line is helpful.

Once you have all the technical requirements in place, it's time to create the project.

## Creating a project in Storyblok
First of all, let's create a basic Storyblok space for this project!

- Log in to Storyblok and create a new space by clicking **Add Space**:

![Add Space](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vydlt3lp7q11n4121al2.png)

- Select the **New Space** option, enter a name for the space, and select the server location (EU by default):

![New Space](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s0yocylmn7fjtnnxsygx.png)

You can review the structure and sample content details. By default, you'll find the following components:

- A Home page in the `Content` section:

![Content Section](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zefzbdb3pkn9r3pykaqi.png)

When you access it, you will see the "Welcome to the Visual Editor" page by default, and the components of the page in the right sidebar:

![Welcome to the Visual Editor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kdtxw8dtr9csmuxn58ez.png)

As you can see, the Home page has a `body` component with two block elements:
- Teaser: this is a text component. The current value of this field is "Hello world!".
 
![Hello World!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g6szcvpgfvg29dqc2zkq.png)


- Grid: this is a block component, with 3 text elements in it (with the text "Feature 1", "Feature 2", and "Feature 3" text on them).
 
![Grid component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0epoal7ir5ve6mn9u40z.png)

We'll cover this later, but understanding the architecture of data is key to creating the right elements in your front-end development.

## Creating a project in Eleventy
Eleventy does not require you to use any JavaScript framework or library. And you can have a basic project up and running in literally less than 1 minute:

- Open a terminal
- Create a folder for your new project
```
mkdir myproject
```
- Access the project
```
cd myproject
```
- Create an `index.md` file. The `.md` extension is for markdown.
```
echo '# Page header' > index.md
```
- Run Eleventy. This command compiles the index.md file (but `.md` is only one of many valid file extensions) into the output folder(`_site` by default), ready to be upload to your favorite hosting service.
```
npx @11ty/eleventy
```
 > You should get something like this:
```
[11ty] Writing _site/index.html from ./index.md (liquid)
[11ty] Wrote 1 file in 0.03 seconds (v2.0.1)
```
- However, you may want to run Eleventy from a local development server while working on it. Run Eleventy locally with this code:
```
npx @11ty/eleventy --serve
```
> You will get something like this: 
```
[11ty] Writing _site/index.html from ./index.md (liquid)
[11ty] Wrote 1 file in 0.04 seconds (v2.0.1)
[11ty] Watching…
[11ty] Server at http://localhost:8080/
```
- Open `http://localhost:8080` in your browser and you will see your website.


## Setting up and configuring the Eleventy project
So far, so simple, right? Your project should have the following architecture:

```
├── _site
│   └── index.html
└── index.md
```

But let's add a few more things to prepare our project for our Storyblok integration.

We've created our first page, index.html, using one of the valid file extensions (.md). But this is just one of many options you can use for your templates:
- HTML: *.html 
- Markdown: *.md 
- WebC: *.webc 
- JavaScript: *.11ty.js 
- Liquid: *.liquid 
- Nunjucks: *.njk 
- Handlebars: *.hbs 
- Mustache: *.mustache 
- EJS: *.ejs 
- Haml: *.haml 
- Pug: *.pug 
- Custom: *.\*

I will use `.md` and `.njk` in this article, but feel free to use the template languages you feel more comfortable with.

There's plenty of options to use, combine and override the template language. You can find all the details <a href="https://www.11ty.dev/docs/languages/" rel="nofollow noopener noreferrer" target="_blank">in the documentation</a>.

In addition to pages, you can use layouts to wrap other content. So let's open the project in VSCode and let's create a base layout to define the common HTML elements for all pages:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ "{{ title }}" }}</title>
  </head>
  <body>
    {{ "{{ content | safe }}"}}
  </body>
</html>
```

Save this file as `base.njk` in a new `_includes/layouts` folder. Your project now has the following structure:

```
├── _includes
│   └── layouts
│       └── base.njk
├── _site
│   └── index.html
└── index.md
```

Now you can add the `base.njk` layout to your `index.md` page. To do this, add the following information to your existing file

```diff
+ ---
+ layout: layouts/base.njk
+ title: This is my homepage
+ ---

# Page header
```

What have been done here? The content between the two `---` marks is the <strong>front matter</strong>. This is a YAML block at the top of the file and is a standard convention used by many Static Site Generators (SSG). So you're probably familiar with it if you've used other SSGs in the past. The content in the front matter is key-value pairs that provide information on how to generate data for this page.

In this case, two key-value pairs have been added: 
- layout: This is the route of the desired layout to apply to the page.
- title: This is the title of the page. As mentioned earlier in the `base.njk` file, this information will be used in the `<title>` tag. 

### Get the API token from Storyblok
An API token is necessary to follow the next steps. By default, you can see your preview token on the "Welcome to the Visual Editor" page. But you can find it (or create a new one) in `Settings > Access Tokens` as well.

![Access Token on the Welcome to the visual editor page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/etjmro45c22jjdhqkxmy.png)

![Access token in Settings - Access Token section](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u8tnipw5jz8202cd0xha.png)


## How to query the Storyblok data from Eleventy
### Create the GraphQL query
In this article, I will be using the **GraphQL Content Delivery API**. This is is a read-only endpoint and is optimized for fast content delivery.

Use the following endpoint to send GraphQL requests:
```
https://gapi.storyblok.com/v1/api
```

Note that you need to append the region code if you are using different regions. For example:

```
https://gapi-us.storyblok.com/v1/api
```

If you haven't used GraphQL schemas before, I have good news for you! Storyblok has a playground where you can explore the functionalities and create the right query. You can access it using this URL with your Access Token in it:

```
https://gapi-browser.storyblok.com/?token=insert-here-your-access-token
```

In this case, we want to get the Home page data:
- Teaser (Hello world!)
- Grid
  - Feature 1
  - Feature 2
  - Feature 3

So, this will be our query:

```
{
  PageItem(id: "insert-your-home-id") {
    name
    content {
      body
    }
  }
}
```

You can find your Home id running the following query in the playground:
```
{
  PageItems {
    items {
      id
      name
    }
  }
}
```

And it will return the id and the name:
```
{
  "data": {
    "PageItems": {
      "items": [
        {
          "id": xxxxxxxxxx,
          "name": "Home"
        }
      ]
    }
  }
}
```
Ok, now we know what data we want to get from Storyblok. But how to do it?


### Data in Eleventy

Eleventy can use data from several different sources. However, when merging data, there is a priority order for data sources. From highest to lowest priority: 

- Computed data
- Front matter data in a template
- Template data files
- Directory data files
- Front matter data in layouts
- Configuration API global data
- Global data files

The two ways that work best for applying a headless CMS are **Global Data** files and **Configuration API global data**. In this article, I'm going to use the first one.

As you can imagine, this means some code is necessary to execute the API call. Let's create a folder called `utils`. Inside it, create a `storyblok.js` file with the following code:

```js
module.exports = async function fetchAPI(query, { variables, preview } = {}) {
    const res = await fetch("https://gapi.storyblok.com/v1/api", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Token: "xxxxxxxxxxxxxxxxxxxxxxxx",
        Version: preview ? "draft" : "published",
      },
      body: JSON.stringify({
        query,
        variables,
      }),
    });
  
    const json = await res.json();
    if (json.errors) {
      console.error(json.errors);
      throw new Error("Failed to fetch API");
    }
    console.log(json.data)
    return json.data;
  }
```

Hardcoding your API token into your code is not a good practice. So you can store it as an `.env` variable. 

```bash
# .env file
STORYBLOK_TOKEN: xxxxxxxxxxxxxxxxxxxxxxxx
```

However, you will need to install the `dotenv` package to access it.

```bash
npm install dotenv --save-dev
```

You will see this step will generate some more files and folders. Don't worry, we'll manage this later. Now you can modify your `storyblok.js` file to use the `.env` variable:

```diff
+ require('dotenv').config()
module.exports = async function fetchAPI(query, { variables, preview } = {}) {
    const res = await fetch("https://gapi.storyblok.com/v1/api", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
-        Token: "xxxxxxxxxxxxxxxxxxxxxxxx",
+        Token: process.env.STORYBLOK_TOKEN,
        Version: preview ? "draft" : "published",
      },
      body: JSON.stringify({
        query,
        variables,
      }),
    });
  
    const json = await res.json();
    if (json.errors) {
      console.error(json.errors);
      throw new Error("Failed to fetch API");
    }
    console.log(json.data)
    return json.data;
  }
```

At this point, let's create a `_data` folder, and a `homepage.js` file in it with the following code: 

```js
const fetchAPI = require("../utils/storyblok");

module.exports = async function getHomepage () {
  const data = await fetchAPI(`
  {
    PageItem(id: "insert-your-home-id") {
      name
      content {
        body
      }
    }
  }
  `);
  return data?.PageItem;
}
```

The previous code will generate the `homepage` data from Storyblok's API, and it will be accessible from other pages and layouts. 

So let's modify our `index.md` file to use it!

```diff
---
layout: layouts/base.njk
title: This is my homepage
---

- # Page header
+ {{ "{{ homepage.content.body[0].headline }}" }}
+ {{ "{{ homepage.content.body[1].columns[0].name }}" }}
+ {{ "{{ homepage.content.body[1].columns[1].name }}" }}
+ {{ "{{ homepage.content.body[1].columns[2].name }}" }}
```

After following all these steps, you should see something similar to this on your local server:

![Hello World. Feature 1. Feature 2. Feature 3.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ctzpovi3ye2v1d636lsp.png)


## Using the Visual Editor from Storyblok
As seen earlier, the Visual Editor uses a "Welcome to the Visual Editor" by default page. To see the page we've created using Storyblok's data we must set up the preview URL. Eleventy runs locally on `http://localhost:8080`. However, Storyblok requires an https connection to use the visual editor. There are several frameworks offering straightforward methods to use SSL in local development. Unfortunately, it's not that easy on Eleventy. 

However, we can fis this issue with `mkcert` and `local-ssl-proxy`. Before following this process, ensure your local server is stopped. If it's still running, you can stop it by clicking `Control + C`.
- Install mkcert (globally)
```bash
npm install -g mkcert
``` 
- Add mkcert to your local root CAs
```bash
mkcert -install
```
- Generate a certificate for your localhost on your project, signed by mkcert. This will generate two `.pem` files.
```bash
mkcert localhost
```
- Install local-ssl-proxy (globally)
```bash
npm install -g local-ssl-proxy

```
- Run Eleventy locally (and keep this terminal running it!). By default, it will run over the port 8080:
```bash
npx @11ty/eleventy --serve
```
- Open a new terminal and run the proxy SSL server through the port 3000 (and keep it running in this new terminal):
```bash
local-ssl-proxy --source 3000 --target 8080 --cert localhost.pem --key localhost-key.pem  
```

After these steps, open `https://localhost:3000` on your browser and you should be able to see the same content as on `http://localhost:8080`.

With these two local servers running, you can go to Storyblok and change the preview URL. You can set it up directly from the default "Welcome to the Visual Editor" page, in the **"Set up preview URL"** field. Or you can change the preview URL from `Settings > Configuration > Visual Editor`:

![Set up preview URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0jus01xr960l7lssqsuh.png)

But you will probably be a bit disappointed after doing this because you'll get a 404 error after saving it. That's because Storyblok adds the content page path to the URL. So, by default, the visual editor will try to load `https://localhost:3000/home`. To change this, go to the page configuration and set `/` as the Real path:

![Set / as the real path](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/drx2wnhcximgc94ib1nz.png)

And now you have it! You can see the content using the Visual Editor. 

![Home page inside the visual editor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/58ppdpeiglm6923lu64i.png)

## Next steps
After following all these steps, you now know how to create content on Storyblok and consume it from Eleventy using the GraphQL API. However, you will notice that content management is not dynamic. If you change something in Storyblok, it will not be updated on the local server or in the visual editor.

That's the expected behavior because Eleventy executes the GraphQL query at build time. If we serve the page locally, this means that the query won't be updated unless we change a file in our project. Therefore, any changes we make won't be visible on Storyblok until we rebuild the project on our local server.

That's a bit annoying, isn't it? But don't worry. I will show you how to overcome all these inconveniences in my next article (part 3) of this series of **Eleventy and Storyblok** articles.