Create a Blog with Eleventy and Storyblok
Tags: webdev, 11ty, storyblok, javascript
Published: 2024-05-13 14:01:45

*This is the third post in a series of articles called "Eleventy and Storyblok" to learn how to use this combination to create a web project.*

<strong>TL;DR</strong>: Too much text for you? Don't worry! You have all the code described in this tutorial in this template repository: <a href="https://github.com/guaca/11ty-storyblok-template/" target="_blank" rel="noopener">https://github.com/guaca/11ty-storyblok-template/</a>. Keep in mind, however, that you will need to create your <strong>.env</strong> file with the proper variables described in this article for the template to work.<p>

Now that you're familiar with why <a href="https://www.11ty.dev/" target="_blank" rel="noopener">Eleventy</a> + <a href="https://get.storyblok.com/estela-franco-cuenca" target="_blank" rel="noopener">Storyblok</a> is my ideal combination and how to seamlessly query data from Storyblok for use in an Eleventy project, it's time for the next exciting step: let's embark on creating a complete blog site using these two phenomenal technologies!

## Creating the Blog Architecture in Storyblok

The desired blog content architecture has the following levels: 

```
├── Homepage
├── articles
│   └── article-1
│   └── article-2
│   └── article-n
├── categories
│   └── category-1
│   └── category-2
│   └── category-n
└── authors
    └── author-1
    └── author-2
    └── author-n
```

So first you need to create a new site in Storyblok (you already learned how to do this in my <a href="/blog/eleventy-storyblok-2/" target="_blank">previous article</a>). Got it? Then let's create the data architecture and some initial content in it!

### Removing the boilerplate content
Remove the default content that <a href="https://get.storyblok.com/estela-franco-cuenca" target="_blank" rel="noopener">Storyblok</a> adds when you create a new site. We will create the blocks and stories we need from scratch. 

### Creating the initial Blocks: Author and Category
Let's start by creating the `Author` type. To do this, go to the `Block Library` (which should be empty if you have deleted all the default blocks) tab and create a new block named `Author`. Select the option `Content type block`. 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-author-block.webp" alt="Create new block form. The technical name field is filled with Author. The content type block option is checked" loading="lazy"/>

Let's edit it and define the required fields: 
- Name (text)
- Bio (textarea)
- Avatar (asset - images)

Remember to mark them as `Required field` if they are mandatory. 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-author-block-fields.webp" alt="Fields of the Author block: Name, Bio and Avatar" loading="lazy"/>

Let's repeat the process to create the `Category` type. Still in the `Block Library`, create a new `Content type block` type. In this case, the name is `Category`. 

In this case, and to keep it simple for this tutorial, I have only defined one field called (surprise) `Category`.

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-category-block.webp" alt="The Category blok only has a field in it, and it is called Category" loading="lazy"/>

## Creating the first stories: Authors and Categories
Now that we have created two content types, let's create the first stories! To do that, go to the `Content` section (which should be empty) and create a new `Folder`.

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-content-folder.webp" alt="There is a menu under the 'Create new' button with two options: Story or Folder" loading="lazy"/>

Let's start with the `Authors`! During the definition of the folder, restrict the content type to `Author`. I also recommend you to check the `Disable visual editor (Form only)` as we won't use the visual editor in this tutorial.  

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-authors-folder.webp" alt="New Folder form, with the fields Name (Authors), Slug (authors), Parent folder (Root), Content type (Restrict to content types - Author) and Folder content settings (disable visual editor option is checked)" loading="lazy"/>

You're now inside the (empty) Authors folder. Create a new `Story`. The pop-up will ask you for some basic data: 
- Name (required). Let's create the `Author 1`.
- Slug (it's generated based on the name).
- Parent folder. In this case, the Authors folder is the parent folder.  
- Content type (required). Select `Author`.

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-author-story-basic-info.webp" alt="New Content Story form, with the fields Name (Author 1), Slug (author-1), Parent folder (Authors), and Content type (Author)" loading="lazy"/>

In the next screen, you will see the form with the fields that we defined when creating the `Author` type. Fill them and hit `Save`.

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-author-story-fields.webp" alt="Author 1 form, with the fields Name, Bio and Avatar" loading="lazy"/>

You can repeat the process and create some more authors. In my case, I have created another one, called (surprise) `Author 2`.

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/author-stories.webp" alt="Authors folder has two stories in it: Author 1 and Author 2" loading="lazy"/>

Let's now move on and create some `Categories`. You can repeat the process and create the `Categories` folder, which should be restricted to the `Category` content type. Remember to check the `Disable visual editor (Form only)` checkbox as we're not using this feature in this tutorial. 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-categories-folder.webp" alt="New Folder form, with the fields Name (Categories), Slug (categories), Parent folder (Root), Content type (Restrict to content types - Category) and Folder content settings (disable visual editor option is checked)" loading="lazy"/>

Now that you're inside the `Categories` folder, create a new story. In this case, it also asks you for some basic information: 
- Name (required). For this example, I'm super creative and I have created the `Category One`.
- Slug (it's generated from the name).
- Parent folder: Categories.
- Content type: Select `Category`.
- In the next screen, you will see the category form asking for the `Category`. In this basic example, this field is also `Category One`. 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-category-one.webp" alt="New Content Story form, with the fields Name (Category One), Slug (category-one), Parent folder (Categories) and Content type (Category)" loading="lazy"/>

## And what about the Articles?

Yes, I know. This is supposed to be a blog site, but I haven't covered the articles yet, right? That's because we're going step by step. And this is the step to start creating Articles. 

Let's start by creating the `Article` content type. Remember how to do this? Correct! Go to the `Block Library` and create a new `Block`. 
- Technical name: Article. 
- Select block type: Content type block. 
And press `Add Block`.

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-article-block.webp" alt="Create new block form. The technical name field is filled with Article. The Content type block option is checked" loading="lazy"/>

Now edit it to add the fields. 
- Title: Text.
- PublicationDate: Date/Time.
- Author. And that's why we created the authors first! Choose the `Single-Option` type and choose the following source details: 
  - Source: stories
  - Path to the folder of stories: authors/
  - Restrict to content type: Author
- Category. Similar to what we have done for `Author`. It is also a `Single-Option` type and these are the source details: 
  - Source: stories
  - Path to the folder of stories: categories/
  - Restrict to content type: Category
- Body. Select `Markdown`, but check the `Rich-text as default` checkbox. This will allow you to have a WYSIWYG interface, but it will be saved (and served) as Markdown. As I mentioned in the first article of this series, Markdown is one of the template languages that you can use in your project. Therefore, choosing `Markdown` will be easier to manage than using the `Richtext` option.

And you already have the `Article` block created!

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-article-block-fields.webp" alt="Create new block form. The technical name field is filled with Article. The Content type block option is checked" loading="lazy"/>

Let's now create the first article. Go to the `Content` section and create a new `Folder` called `Articles`:
- Name: Articles.
- Slug (it's generated from the Name).
- Parent folder: Root. 
- Content type: Restrict content types to Articles

Once again, remember to disable the visual editor (Form only). 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-article-one-basic-info.webp" alt="Create new content story form with the fields Name (Article 1), Slug (article-1), Parent folder (Articles) and Content type (Article). The Content type block option is checked" loading="lazy"/>

And now you will see the form to create your first article. You've seen that I am being very creative with the content I'm using in this new blog site, so you won't be surprised by my first article: 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-article-one.webp" alt="Create new block form. The technical name field is filled with Article. The Content type block option is checked" loading="lazy"/>

As you can see, the Author and Category were selected from the drop-down list. 

You can repeat these steps to generate a few more articles. I have created a total of 3 articles for this tutorial. 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/articles-storyblok.webp" alt="The Articles folder with tree articles in it: Article 1, Article 2 and Article 3" loading="lazy"/>

## Creating a Homepage

Last but not least, we need a homepage for our brand new blog site. You've seen how creative I've been with the rest of the content. So, the homepage can't be any different!

Let's create the `Block` first. As usual, go to the `Block library` and create a new one with the following information: 
- Technical name: Page (I used this, but feel free to use whatever works better for you).
- Select block type: Content type block.

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-page-block.webp" alt="Create new block form. The technical name is set to Page and 'content type block' is the selected block type" loading="lazy"/>

And now, let's define the fields:
- Heading: Text.
- Subtitle: Text.
- Highlights: Multi-Options
  - Source: Stories
  - Path to folder of stories: articles/
  - Restrict to content type: Article
  - Minimum: 3
  - Maximum: 5

So, our homepage will have a heading, a subtitle and a list of the top articles. 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-page-block-fields.webp" alt="Create new block form. The technical name is set to Page and 'content type block' is the selected block type" loading="lazy"/>


Let's now create the homepage content. Go to `Content`, create a new Story and the pop-up will ask you for some basic information: 
- Name (required)
- Slug (it's generated from the name)
- Parent folder: Root
- Content type: Page

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-content-home.webp" alt="New content story form. The name is set to Home, slug is home, parent folder is root and content type is set to page" loading="lazy"/>

On the next screen, you will see the visual editor enabled. This is happening because Storyblok does offer to disable the visual editor when creating a Folder, but not when creating a Story. Let's disable it as we're not going to use it in this tutorial. 

To do it, access the `Entry configuration` from the navbar above the visual editor. This will open a pop-up and you will see the `Edit mode` options. Check the `Form-only` one. 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/disable-visual-editor.webp" alt="There is a pop-up named 'Entry configuration'. The Edit mode is set to 'form-only'" loading="lazy"/>

Ok, it looks easier this way, right? Let's fill the fields with my already-famous creative content: 
- Heading: This is the Homepage
- Subtitle: Welcome to the homepage of this amazing web site!
- Highlights: In my example, I created 3 articles and I will select them all. An important detail: it's a reordable drag-and-drop list, so feel free to sort them however you like.

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-content-home-fields.webp" alt="Home page form, with the heading (This is the homepage), subtitle (Welcome to the homepage of this amazing web site!) and highlights (article 3, article 1 and article 2)" loading="lazy"/>

And that's it! You have your headless CMS created and ready to generate new content. 

## Wait. And what about SEO?
If this question has crossed your mind, THANK YOU! Because we need more people who care about making our sites "SEO friendly" since the first stage of the project. 

And if you haven't thought about it, don't worry. I'm here to add this topic just in time.

### Create the SEO block
Let's go to the `Block Library` and create a new nestable block. Call it `SEO`. Define a minimum and maximum of 1 block.

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-seo-block.webp" alt="Create new block form. The technical name is set to SEO, and the 'nestable block' is the selected block type." loading="lazy"/>


Add the following fields: 
- title: text.
- description: text.
- canonical: text.
- noindex: boolean. The default value will be disabled.
- og_title: text.
- og_description: text. 
- og_image: asset (image).

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/create-seo-block-fields.webp" alt="The pop-up shows the fields that have been created: title, description, noindex, canonical, og_title, og_description and og_image." loading="lazy"/>

### Add the SEO block to the other blocks
We want to be able to define the proper SEO and Social Media data for all the stories we have already created, so we need to add it to the related content types: 
- Article
- Author
- Category
- Page

Once you've added the SEO block to them, you can access the content and add the SEO data: 

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/add-seo-block-article.webp" alt="The pop-up shows the fields that have been created: title, description, noindex, canonical, og_title, og_description and og_image." loading="lazy"/>

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/add-seo-block-article-one.webp" alt="The pop-up shows the fields that have been created: title, description, noindex, canonical, og_title, og_description and og_image." loading="lazy"/>

## Creating the Eleventy project
Alright, you've got the content, but now it's time to build the site that will put that content to use, right? The following steps will help you achieve the desired project structure:

```
├── src
│   └── _data
│   │    └── articles.js
│   │    └── authors.js
│   │    └── categories.js
│   │    └── home.js
│   │    └── metadata.json
│   └── includes
│   │    └── layouts
│   │    │    └── base.njk
│   │    └── footer.njk
│   │    └── head.njk
│   │    └── header.njk
│   └── public
│   │   └── css
│   │       └── styles.css
│   └── utils
│   │   └── storyblok.js
│   └── article-page.njk
│   └── articles-list.njk
│   └── author-page.njk
│   └── authors-list.njk
│   └── category-page.njk
│   └── categories-list.njk
│   └── index.njk
└── eleventy.js
└── .env
└── .gitignore
└── package.json
```

### Setting up and configuring the Eleventy project
You already learned how to do this in my <a href="/blog/eleventy-storyblok-2/">previous article</a>, so I'll make it faster this time.

> #### 1. Create and access your project (and open it using VSCode)

>  ```
>  mkdir myproject
>  cd myproject
>  code .
>  ```

> #### 2. Create your `package.json` file in the root directory and paste the following lines:

>```js
>{
>  "scripts": {
>    "build": "npx @11ty/eleventy",
>    "start": "npx @11ty/eleventy --serve"
>  },
>  "devDependencies": {
>    "@11ty/eleventy": "^2.0.1",
>    "dotenv": "^16.4.1"
>  }
>}
>```

> #### 3. Install dependencies
>```
>npm install
>```
>
> #### 4. Create the `.env` file in the root directory
>
>```bash
>STORYBLOK_API_KEY="[your-storyblok-token-goes-here]"
>DOMAIN="[your-website-domain-goes-here]"
>```
>
> #### 5. Create the `eleventy.js` config file
>
>```js
>require('dotenv').config()
>module.exports = function (eleventyConfig) {
>  // Add the .env domain as GlobalData to use in SEO meta data
>  eleventyConfig.addGlobalData("domain", process.env.DOMAIN );
>  
>  // Filter articles by Author slug
>  eleventyConfig.addFilter("filterArticlesByAuthor", function(articles, authorFullSlug) {
>    const filteredArticles = articles.filter(article => {
>      return article.content.Author.fullSlug === authorFullSlug;
>    });
>    // Sort the filtered articles by PublicationDate in descending order
>    filteredArticles.sort((a, b) => {
>      return new Date(b.content.PublicationDate) - new Date(a.content.PublicationDate);
>    });
>    return filteredArticles;
>  });
>  
>  // Filter articles by Category slug
>  eleventyConfig.addFilter("filterArticlesByCategory", function(articles, categoryFullSlug) {
>    const filteredArticles = articles.filter(article => {
>      return article.content.Category.fullSlug === categoryFullSlug;
>    });
>    // Sort the filtered articles by PublicationDate in descending order
>    filteredArticles.sort((a, b) => {
>      return new Date(b.content.PublicationDate) - new Date(a.content.PublicationDate);
>    });
>    return filteredArticles;
>  });
>
>  // Sort the authors  by name: 
>  eleventyConfig.addFilter("sortAuthorsByName", function(authors) {
>    return authors.sort((a, b) => a.content.Name.localeCompare(b.content.Name));
>  });
>
>  // Pass Trhough Copy
>  eleventyConfig.addPassthroughCopy({
>    "./src/public/": "/",
>  });
>
>  return {
>    dir: {
>      input: 'src'
>    },
>  };
>};  
>```
>
>What have we done here? Let's go one by one: 
>- Initializing `dotenv`
>  This initial line populates env variables into process.env and makes it available in the config file. 
>
>- Add Global Data 
>  We're adding the `DOMAIN` enviroment variable as Global Data, ensuring it's available throughout the project.
>
>- Filter articles by Author
>  We've defined a new filter to retrieve articles authored by a specific author. This filter will be applied on the Author's page to list all articles authored by that individual. Additionally, the articles will be sorted by PublicationDate in descending order.
>
>- Filter articles by Category
>  Similar to the previous filter, here we're filtering articles belonging to a specific Category. This filter will be utilized on the Category's page to list all articles within that particular Category. Once again, the articles are sorted by PublicationDate in descending order.
>
>- Sort authors by name
>  This filter enables us to arrange the list of authors alphabetically by their names.
>
> #### 6. Create the `src > _data > metadata.json` file
>
>```json
>{
>    "title": "My site",
>    "language": "en",
>    "description": "Welcome to my blog site!",
>    "image": "[fallback-image-for-social-media-goes-here]"
>}
>```
>
> #### 7. Create the `src > _includes > head.njk` file 
>
>```njk
><head>
>  <meta charset="UTF-8">
>  <meta http-equiv="X-UA-Compatible" content="IE=edge">
>  <meta name="viewport" content="width=device-width, initial-scale=1.0">
>  <title>{{ title or metadata.title }}</title>
>  <link rel="stylesheet" href="/css/styles.css">
>  <meta name="description" content="{{ description or metadata.description }}">
>  <link rel="canonical" href="{{ canonical or domain+page.url }}">
>  {% if noindex == "true" %}
>    <meta name="robots" content="noindex">
>  {% endif %}
>  <meta property="og:title" content="{{ og_title or title or metadata.title }}">
>  <meta property="og:description" content="{{ og_description or description or metadata.description }}">
>  <meta property="og:image" content="{{ og_image or metadata.image }}">
>  <meta name="twitter:card" content="summary_large_image">
>  <meta name="twitter:title" content="{{ og_title or title or metadata.title }}">
>  <meta name="twitter:image" content="{{ og_image or metadata.image }}">
></head>
>```
>
>
>This might seem a bit overwhelming, right? It's because we're integrating Nunjucks variables and conditionals (using curly braces) with HTML tags. However, it's just as straightforward and incredible as you saw in the previous code.
>
>For example `<link rel="canonical" href="{{ canonical or domain+page.url }}">` generates a canonical link with the canonical URL sourced from the frontmatter data. If that's missing, it constructs a canonical URL by combining the domain (Global Data) with the URL of the current page. BOOM!
>
> #### 8. Create the `src > _includes > header.njk` file 
>
>```html
><header>
>  <nav class="header-nav">
>    <a class="header-home" href="/">My site</a>
>    <ul class="header-list">
>        <li class="header-item"><a href="/articles/">Articles</a></li>
>        <li class="header-item"><a href="/authors/">Authors</a></li>
>        <li class="header-item"><a href="/categories/">Categories</a></li>
>    </ul>
>  </nav>
></header>
>```
>
> #### 9. Create the `src > _includes > footer.njk` file 
>
>```njk
>  <footer>
>    © 2024 - All Rights Reserved
>  </footer>
>```
>
> #### 10. Create the `src > _includes > layouts > base.njk` file
>
>
>```njk
><!DOCTYPE html>
><html lang="{{ metadata.language }}">
>{% include "head.njk" %}
><body>
>  {% include "header.njk" %}
>  <main style="flex-grow:1;">
>    {{ content | safe }}
>  </main>
>  {% include "footer.njk" %}
></body>
></html>
>
>```
>That seemed a bit weird, didn't it? Let's break it down step by step. 
>- The `lang` attribute in the HTML comes from the metadata.json file, where we've set the language. 
>- We utilize `include` to pull in the templates we've created (`head`, `header` and `footer`).
>
> #### 11. Create the `src > utils > storyblok.js` file 
>
>```js
>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: process.env.STORYBLOK_TOKEN,
>        Version: "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;
>  }
>```
>This code may sound familiar because we've already covered it in the <a href="/blog/eleventy-storyblok-2/" target="_blank">previous article</a>. Remember, I'm using the `published` version, but you might opt for the `draft` version if this template isn't intended for a production site. 
>
> #### 12. Create your `src > _data > articles.js` file. 
> It will generate the global `articles` data file, which you'll utilize to create your article pages. This is the code for this file: 
>
>```js
>const fetchAPI = require("../utils/storyblok.js");
>
>
>module.exports = async function getArticles () {
>  const data = await fetchAPI(`
>  {
>    ArticleItems {
>      items {
>        full_slug
>        content {
>          Body
>          PublicationDate
>          Title
>          Author {
>            name
>            fullSlug
>          }
>          Category {
>            name
>            fullSlug
>          }
>          SEO
>        }
>      }
>    }
>  }  
>  `);
>  return data?.ArticleItems.items;
>}
>```
>
> #### 13. Create the `src  > article-pages.njk` file 
>
>```njk
>---
>pagination:
>    data: articles
>    size: 1
>    alias: article
>permalink: "{{ article.full_slug }}/"
>layout: layouts/base.njk
>templateEngineOverride: njk,md
>eleventyComputed: 
>    title: "{{ article.content.SEO[0].title }}"
>    description: "{{ article.content.SEO[0].description }}"
>    canonical: "{{ article.content.SEO[0].canonical }}"
>    noindex: "{{ article.content.SEO[0].noindex }}"
>    og_title: "{{ article.content.SEO[0].og_title }}"
>    og_description: "{{ article.content.SEO[0].og_description }}"
>    og_image: "{{ article.content.SEO[0].og_image.filename }}"
>---
><article>
>    <h1>{{ article.content.Title }}</h1>
>    <a href="/{{ article.content.Author.fullSlug }}">{{ article.content.Author.name }}</a>
>    <span> - </span>
>    <a href="/{{ article.content.Category.fullSlug }}">{{ article.content.Category.name }}</a>
>    <p>{{ article.content.PublicationDate }}</p>
>    <div>
>        {{ article.content.Body | safe }}
>    </div>
></article>
>```
>
>Yes, that's a lot of SEO and Social Media metadata, but it's necessary! You might be wondering why this data is under `eleventyComputed` instead of being set directly, like we did with `permalink`.
>
>And that's a great question! We can only use variables and shortcodes in `permalink` and `eleventyComputed`. Therefore, we don't need to set the permalink under eleventyComputed, but the rest of the custom fields must be placed there to ensure they are dynamicly updated."
>
> #### 14. Create the `src > articles-list.njk` file 
> This template will list all the published articles. 
>
>```njk
>---
>permalink: "/articles/"
>layout: layouts/base.njk
>title: "Articles"
>description: "This is the list of Articles"
>---
><div>
>    <h1>Articles</h1>
>    <ul>
>        {% for article in articles %}
>        <li><a href="/{{ article.full_slug }}">{{ article.content.Title }}</a></li>
>        {% endfor %}
>    </ul>
></div>
>```
>
> #### 15. Repeat steps 12 through 14, but this time creating the files related to `Authors`
>
>- Filename: src > _data > authors.js
>```js
>const fetchAPI = require("../utils/storyblok.js");
>
>module.exports = async function getAuthors () {
>  const data = await fetchAPI(`
>  {
>    AuthorItems {
>      items {
>        full_slug
>        content {
>          Name
>          Bio
>          Avatar {
>            filename
>          }
>          SEO
>        }
>      }
>    }
>  }
>  `);
>  return data?.AuthorItems.items;
>}
>```
>
>- Filename: src > author-page.njk
>```njk
>---
>pagination:
>    data: authors
>    size: 1
>    alias: author
>permalink: "{{ author.full_slug }}/"
>layout: layouts/base.njk
>eleventyComputed: 
>    title: "{{ author.content.SEO[0].title }}"
>    description: "{{ author.content.SEO[0].description }}"
>    canonical: "{{ author.content.SEO[0].canonical }}"
>    noindex: "{{ author.content.SEO[0].noindex }}"
>    og_title: "{{ author.content.SEO[0].og_title }}"
>    og_description: "{{ author.content.SEO[0].og_description }}"
>    og_image: "{{ author.content.SEO[0].og_image.filename }}"
>---
>
><div>
>    <div class="author-info">
>        <img class="author-avatar" src="{{ author.content.Avatar.filename }}">
>        <h1 class="author-name">{{ author.content.Name }}</h1>
>    </div>
>        <p>{{ author.content.Bio }}</p>
>    <h2>Articles by {{ author.content.Name }}</h2>
>    
>    {% set targetAuthorFullSlug = author.full_slug %}
>    {% set filteredArticles = articles | filterArticlesByAuthor(targetAuthorFullSlug) %}
>    <ul>
>        {% for article in filteredArticles %}
>            <li>
>                <article>
>                    <h3><a href="/{{ article.full_slug }}">{{ article.content.Title }}</a></h3>
>                    <span>Publication Date: {{ article.content.PublicationDate }}</span>
>                </article>
>            </li>
>        {% else %}
>            <p>No articles found for this author.</p>
>        {% endfor %}
>    </ul>
>
></div>
>```
>
>- Filename: src > authors-list.njk
>
>```njk
>---
>permalink: "/authors/"
>layout: layouts/base.njk
>title: "Authors"
>description: "This is the list of Authors"
>---
><div>
>    <h1>Authors</h1>
>    <ul>
>        {% for author in authors | sortAuthorsByName %}
>        <li><a href="/{{ author.full_slug }}">{{ author.content.Name }}</a></li>
>        {% endfor %}
>    </ul>
></div>
>```
>
>
> #### 16. Repeat steps 12 through 14, but this time creating files related to `Categories`
>- Filename: src > _data > categories.js
>```js
>const fetchAPI = require("../utils/storyblok.js");
>
>module.exports = async function getCategories () {
>  const data = await fetchAPI(`
>  {
>    CategoryItems {
>      items {
>        full_slug
>        content {
>          Category
>          SEO
>        }
>      }
>    }
>  }
>  `);
>  return data?.CategoryItems.items;
>}
>```
>- Filename: src > category-page.njk
>
>```njk
>---
>pagination:
>    data: categories
>    size: 1
>    alias: category
>permalink: "{{ category.full_slug }}/"
>layout: layouts/base.njk
>eleventyComputed: 
>    title: "{{ category.content.SEO[0].title }}"
>    description: "{{ category.content.SEO[0].description }}"
>    canonical: "{{ category.content.SEO[0].canonical }}"
>    noindex: "{{ category.content.SEO[0].noindex }}"
>    og_title: "{{ category.content.SEO[0].og_title }}"
>    og_description: "{{ category.content.SEO[0].og_description }}"
>    og_image: "{{ category.content.SEO[0].og_image.filename }}"
>---
><div>
>    <h1>{{ category.content.Category }}</h1>
>
>    {% set targetCategoryFullSlug = category.full_slug %}
>    {% set filteredArticles = articles | filterArticlesByCategory(targetCategoryFullSlug) %}
>    <ul>
>        {% for article in filteredArticles %}
>            <li>
>                <article>
>                    <h3><a href="/{{ article.full_slug }}">{{ article.content.Title }}</a></h3>
>                    <span>Publication Date: {{ article.content.PublicationDate }}</span>
>                </article>
>            </li>
>        {% else %}
>            <p>No articles found for the author with Full Slug "{{ targetAuthorFullSlug }}".</p>
>        {% endfor %}
>    </ul>
></div>
>```
>
>- Filename: src > categories-list.njk
>```njk
>---
>permalink: "/categories/"
>layout: layouts/base.njk
>title: "Categories"
>description: "This is the list of Categories"
>---
><div>
>    <h1>Categories</h1>
>    <ul>
>        {% for category in categories %}
>        <li><a href="/{{ category.full_slug }}">{{ category.content.Category }}</a></li>
>        {% endfor %}
>    </ul>
></div>
>```
> #### 17. Create the `Home` related files
>  In this case, we need the `Home id`. You can find it using the GAPI browser, <a href="/blog/eleventy-storyblok-2/#create-the-graphql-query" target="_blank">as I explained in my previous article</a>.  
>
>  - Once you have the home id, add the variable in the `.env` file:
>
>```
>   STORYBLOK_API_KEY="[your-storyblok-token-goes-here]"
>   DOMAIN="[your-website-domain-goes-here]"
>   HOME_ID="[your-home-id]"
>```
>
>- Create the `src > _data > home.js` file and add the following code in it: 
>```js
>require('dotenv').config()
>const fetchAPI = require("../utils/storyblok");
>
>module.exports = async function getHome () {
>  const data = await fetchAPI(`
>  {
>    PageItem(id: "${process.env.HOME_ID}") {
>      content {
>        Heading
>        Subtitle
>        Highlights {
>          fullSlug
>          name
>          content
>        }
>        SEO
>      }
>    }
>  }  
>  `);
>  return data?.PageItem;
>}
>```
>- Create the `src > index.njk` file: 
>
>```njk
>---
>pagination:
>    data: home
>    size: 1
>permalink: "/"
>layout: layouts/base.njk
>eleventyComputed: 
>    title: "{{ home.content.SEO[0].title }}"
>    description: "{{ home.content.SEO[0].description }}"
>    canonical: "{{ home.content.SEO[0].canonical }}"
>    noindex: "{{ home.content.SEO[0].noindex }}"
>    og_title: "{{ home.content.SEO[0].og_title }}"
>    og_description: "{{ home.content.SEO[0].og_description }}"
>    og_image: "{{ home.content.SEO[0].og_image.filename }}"
>---
><h1>{{ home.content.Heading }}</h1>
><p class="home-subtitle">{{ home.content.Subtitle }}</p>
>
><h2>Highlighted Articles</h2>
><ul>
>    {% for article in home.content.Highlights %}
>    <li><a href={{ article.fullSlug }}>{{ article.content.Title }}</a></li>
>    {% endfor %}
></ul>
>```
>
>
> #### 18. Create the `src > public > css > styles.css` file
>```css
>/* Colors */
>:root {
>    --darkgray: #202124;
>    --purple: #713973;
>    --white: #FFF;
>}
>body {
>  background-color: var(--white);
>  color: var(--darkgray);
>  display: flex;
>  flex-direction: column;  
>  font-family: system,-apple-system,system-ui,BlinkMacSystemFont,Roboto-Light,sans-serif;
>  font-size: clamp(1.1em, calc(0.25vw + 0.8em), 1.25em);
>  margin: 0;
>  min-height: 100vh;
>  padding: 0;
>}
>header {
>  display: flex;
>  justify-content: space-between;
>  border-bottom: 1px solid var(--darkgray);
>}
>footer {
>  border-top: 1px solid var(--darkgray);
>  text-align: center;
>  padding: 1em;
>}
>main {
>  flex-grow: 1;
>  margin: 4em auto 0 auto;
>  width: min(60em, 90%);
>}
>.header-home {
>  text-decoration: none;
>  font-size: 1.5em;
>  font-weight: 600;
>  color: var(--darkgray);
>  &:hover {
>      color: var(--purple);
>  }
>}
>.header-nav {
>  display: flex;
>  align-items: center;
>  width: min(60em, 90%);
>  margin: 0 auto;
>}
>.header-list {
>  margin-right: 1em;
>  padding: 0;
>  list-style: none;
>  float: left;
>  margin-left: 1em;
>  display: flex;
>  justify-content: flex-end;
>  flex-grow: 1;
>  list-style: none;
>}
>.header-item {
>  font-size: 1em;
>  & a {
>      margin: 0 1em;
>      color: var(--darkgray);
>      text-decoration: none;
>      &:hover {
>          color: var(--purple);
>      }
>  }
>}
>.author-avatar {
>    width: 75px;
>}
>.author-name {
>    margin-left: 1.5em;
>}
>.author-info {
>    display: flex;
>    flex-direction: row;
>}
>p:empty {
>  display: none;
>}
>```

## Run it locally
You're nearly finished! With your content in Storyblok and your code stored in a local repository, it's time to run it locally and ensure everything appears as intended.
```
npm start
```
Executing this command will run Eleventy locally, making your brand new (local) site accessible at port 8080. If port 8080 is already in use, the correct port will be displayed in the console.

## Demo and Template Repository
You can see this blog site in action here: <a href="https://11ty-storyblok.netlify.app/" target="_blank" rel="noopener">https://11ty-storyblok.netlify.app</a>

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/home.webp" alt="Blog site homepage." loading="lazy"/>

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/articles-list.webp" alt="Blog site articles list." loading="lazy"/>

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/article-page.webp" alt="Blog site article page." loading="lazy"/>

<img class="tmpl-img" src="https://estelafranco.com/img/11ty-storyblok-blog/author-page.webp" alt="Blog site author page." loading="lazy"/>

In addition, you have all the code described in this tutorial in this template repository: <a href="https://github.com/guaca/11ty-storyblok-template/" target="_blank" rel="noopener">https://github.com/guaca/11ty-storyblok-template/</a>. So, you can create your own blog site by clicking the "Use this template" button. 

## What's next?
This tutorial has guided you through configuring and building a blog site, utilizing Storyblok for data storage and Eleventy for page generation. Once completed, you can deploy your Eleventy project to your preferred Jamstack hosting service. By setting up appropriate workflows and hooks, you can ensure that your site updates automatically whenever you publish a new story in Storyblok.

You have all these steps defined in this <a href="https://estelafranco.com/blog/storyblok-astro/#4.-deploy-the-project-on-netlify" target="_blank">article</a>. 

## Can I use the Storyblok Visual Editor with Eleventy?
Long answer short? Yes but it's not straightforward. 

Long answer: Yes, you can leverage it by configuring an edge function to refresh the API/GAPI data from Storyblok upon updates. Eleventy generates data during build time. Consequently, even after saving new data, the data won't be updated in the Visual Editor preview unless the edge function rebuilds it. 

Storyblok's visual editor seamlessly integrates with other frameworks like Next or Nuxt, allowing you to observe content updates instantly, even without saving changes. However, this functionality is not supported by static site generators.

In the next article in this series, I will use Netlify Edge functions to leverage the Storyblok Visual Editor to make working with the editor easier and more intuitive. Stay tuned!