DEV Community

Cover image for Create a Blog with Eleventy and Storyblok
Estela Franco
Estela Franco

Posted on • Originally published at estelafranco.com

Create a Blog with Eleventy and Storyblok

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.

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

Now that you're familiar with why Eleventy + Storyblok 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
Enter fullscreen mode Exit fullscreen mode

So first you need to create a new site in Storyblok (you already learned how to do this in my previous article). Got it? Then let's create the data architecture and some initial content in it!

Removing the boilerplate content

Remove the default content that Storyblok 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.

Create new block form. The technical name field is filled with Author. The content type block option is checked

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.

Fields of the Author block: Name, Bio and Avatar

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.

The Category blok only has a field in it, and it is called Category

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.

There is a menu under the 'Create new' button with two options: Story or Folder

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.

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)

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.

New Content Story form, with the fields Name (Author 1), Slug (author-1), Parent folder (Authors), and Content type (Author)

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.

Author 1 form, with the fields Name, Bio and Avatar

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

Authors folder has two stories in it: Author 1 and Author 2

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.

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)

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.

New Content Story form, with the fields Name (Category One), Slug (category-one), Parent folder (Categories) and Content type (Category)

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.

Create new block form. The technical name field is filled with Article. The Content type block option is checked

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!

Create new block form. The technical name field is filled with Article. The Content type block option is checked

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

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

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:

Create new block form. The technical name field is filled with Article. The Content type block option is checked

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.

The Articles folder with tree articles in it: Article 1, Article 2 and Article 3

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.

Create new block form. The technical name is set to Page and 'content type block' is the selected block type

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.

Create new block form. The technical name is set to Page and 'content type block' is the selected block type

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

New content story form. The name is set to Home, slug is home, parent folder is root and content type is set to page

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.

There is a pop-up named 'Entry configuration'. The Edit mode is set to 'form-only'

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.

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)

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.

Create new block form. The technical name is set to SEO, and the 'nestable block' is the selected block type.

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

The pop-up shows the fields that have been created: title, description, noindex, canonical, og_title, og_description and og_image.

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:

The pop-up shows the fields that have been created: title, description, noindex, canonical, og_title, og_description and og_image.

The pop-up shows the fields that have been created: title, description, noindex, canonical, og_title, og_description and og_image.

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
Enter fullscreen mode Exit fullscreen mode

Setting up and configuring the Eleventy project

You already learned how to do this in my previous article, 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:

{
 "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

STORYBLOK_API_KEY="[your-storyblok-token-goes-here]"
DOMAIN="[your-website-domain-goes-here]"

5. Create the eleventy.js config file

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

{
   "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

<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

<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

 <footer>
   © 2024 - All Rights Reserved
 </footer>

10. Create the src > _includes > layouts > base.njk file

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

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 previous article. 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:

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

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

---
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
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
---
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
---
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
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
---
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
---
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, as I explained in my previous article.

  • 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:
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:
---
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

/* 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
Enter fullscreen mode Exit fullscreen mode

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: https://11ty-storyblok.netlify.app

Blog site homepage.

Blog site articles list.

Blog site article page.

Blog site author page.

In addition, you have all the code described in this tutorial in this template repository: https://github.com/guaca/11ty-storyblok-template/. 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 article.

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!

Top comments (0)