⚠️Draft Content

Storyblok Raises $80M Series C - Read News

What’s the True Total Price of Enterprise CMS? Find out here.

Skip to main content

Add a headless CMS to Jekyll

The idea of Jekyll is quite simple, it's all about transforming your plain text into static websites and blogs. So in this quick walkthrough, we will have a look at how we can use the data from the Storyblok API with a Jekyll project to create some pages. At the end of this article, you will have a Jekyll project which renders components filled with data from the Storyblok API.

Headless CMS for Jekyll Storyblok

We're about to use Storyblok as headless CMS - so what is it?

Let me start with a short explanation of Storyblok: It is a hosted headless CMS in which you can create nested components per content entry. The author of one content entry, therefore, can create components that act as Content-type like articles or products but also easily can create nestable components to create landing pages - but would allow you to add Storyblok to existing solutions to enrich your current content as well.

Storyblok Explained

What are we going to build?

During this article, we will use the default component page which acts as layout/content-type and the nested components teaser, grid and feature to create a sample layout for landing pages. You will receive this setup during this tutorial, however, if you've already created a space that structure is already available in the content entry with the name "home".

Img

Let’s start with Jekyll

Requirements

  • GNU/Linux, Unix, or macOS
  • Ruby version 2.6 or above, including all development headers
  • RubyGems
  • GCC (gcc -v) and Make (make -v)

Installation

Install Jekyll and Bundler gems through RubyGems (RubyGems is a package manager for Ruby).

        
      gem install jekyll bundler
    

Now you should be able to run jekyll -v and have the return of the jekyll version installed on your development site, in this tutorial we use version 4.0.0, with everything set up run the command to create your Jekyll project as:

        
      jekyll new myjekyll
    

Finally - let's startup your Jekyll project:

        
      cd myjekyll && bundle exec jekyll serve
    

By default you should now be able to browse http://127.0.0.1:4000/

If you're facing any troubles feel free to leave a comment below or check out the Jekyll Quickstart.

Storyblok ruby sdk

Install the Storyblok Ruby SDK to access the API

By creating a new Storyblok space you should already have a basic setup of content components including teaser, grid, and feature.

To access data from Storyblok we will have to call the API to receive that data. The easiest way to do that in Jekyll is to write a custom plugin and write some lines of ruby. We will use the Storybloks Ruby SDK by simply adding to load that data:

        
      gem "storyblok"
    

to your Gemfile and followed by executing

        
      bundle install
    

Let's create a Storyblok space

If you're not registered already simply sign up using the web-interface. You will be guided through the creating of your first space.

Img

The default set of components and a content entry called "home" will be generated for you. You will also see the on-boarding with some code examples in different programming languages by clicking on that "home" content entry.

You will also see the draft token (sometimes called private token) which allows us to load draft versions of your content, you can copy that from one of the code examples or from your space dashboard. We will need this token in the next step, because we're about to load that!

Img

Create a simple plugin to generate pages according to your content

To allow a generation of pages according to your content in Storyblok, we will write a custom plugin to load the data from the API. You can customize it as you want and transform the data as you need it. It uses the Storyblok Content Delivery API and the endpoints Links & Story in this example.

We've named the file _plugins/storyblok_page.rb, but you can name it as you want.

        
      require "storyblok"

module Jekyll
  class StoryblokPage < Page
    def initialize(site, base, dir, story, links)
      @site = site
      @base = base
      @dir = dir
      @name = 'index.html'

      # We will use the root component (eg. content type) as layout
      layout = story['content']['component']

      self.process(@name)
      # Jekyll provides the processed layouts in the site.layouts hash, so we will use it here!
      # This makes it possible to use gem-based Jekyll themes.
      self.data    = site.layouts[layout].data.dup
      self.content = site.layouts[layout].content.dup

      # Assign the received data from the Storyblok API as variables
      self.data['story'] = story
      self.data['title'] = story['name']
      self.data['links'] = links
    end
  end

  class StoryblokPageGenerator < Generator
    safe true

    def generate(site)
      @storyblok_config = site.config['storyblok']
      raise 'Missing Storyblok configuration in _config.yml' unless @storyblok_config

      link = client.links['data']['links']
      stories = client.stories['data']['stories']

      stories.each do |story|
        create_page(site, story, link)
      end
    end

    private

    def client
      @client ||= ::Storyblok::Client.new(
        token: @storyblok_config['token'],
        version: @storyblok_config['version']
      )
    end

    def create_page(site, story, link)
      site.pages << StoryblokPage.new(site, site.source, story['full_slug'], story, link)

      if story['full_slug'] == 'home'
        site.pages << StoryblokPage.new(site, site.source, '', story, link)
      end
    end
  end
end
    

Add your preview token to your config.yml

        
      # Settings for your Storyblok space
storyblok:
  token:   "YOUR_PREVIEW_TOKEN"
  # Can be either `draft` or `published`
  version: draft
    

Our first custom layout

Since the default Jekyll setup ships with the minima theme we don't see the _layouts folder yet. But what we see is an error _layouts/page.html in the console.

By accessing the API of Storyblok you can see that page is the default root "component" (eg. "Content-Type"). You can create as many content-types as you want to allow different layouts and fieldsets (think of something like: post, project or similar). Let's focus on the _layouts/page.html for now.

        
      <html>
  <head>
    <title>{{page.story.name}}</title>
  </head>
  <body>
    <div class="root">
      <strong>Content-Entry including Meta information:</strong>
      <pre>{{ page.story }}</pre>
      <strong>Editorial Content</strong>
      <pre>{{ page.story.content }}</pre>
    </div>
  </body>
</html>
    

The custom plugin loads the layout according to the first component in the content. As mentioned by default Storyblok comes with a component/content-type named "page", you can create new components such as this as you want. By creating a file _layouts/page.html with the above content, you can see how the data you receive from Storyblok actually looks like.

The page component contains a field called body which is a simple array of other (now nestable) components. Let's try to iterate through that array called body and output the containing components:

        
      <div class="root">
  <strong>Components</strong>
  <ul>
  {% for blok in page.story.content.body %}
    <li>{{ blok.component }}</li>
  {% endfor %}
  </ul>
</div>
    

Use Jekylls Includes to create reusable components

Jekyll ships with the possibility to define _includes which are reusable code snippets. In Storyblok we call those reusable parts simply "Components", actually everything in Storyblok is a component - some are acting as content-types other are nestable. The content we received from the API already has e components (teaser, grid and feature) of which we saw two already above. Now let's switch to Jekylls Includes to allow dynamically add those new components. To include them in the page layout we will have to update our page.html before:

        
      <div class="root">
  {% for blok in page.story.content.body %}
    {% include {{blok.component}}.html blok=blok %}
  {% endfor %}
</div>
    

We're about to include teaser.html and grid.html as those components we could see before, to allow Jekyll to include them we need to create the files in the folder _includes.

Create _includes/teaser.html and _includes/grid.html

Above we can see that all information for one such component is in the blok property and therefore we are able to access all information of a component like shown below.

Simply create grid.html and teaser.html in the _includes folder with the content below:

        
      <div>
  This is a <strong>{{blok.component}}</strong> and has those fields: {{blok}}
</div>
    

Use the fields of the teaser component

We can now see that every field defined in Storyblok for one component can be used as a property in Jekylls Liquid template files. Let's access the headline field of the teaser component and output it as an actual headline, simply change the content of the _includes/teaser.html:

        
      <div class="teaser">
  <h2>{{blok.headline}}</h2>
</div>
    

The Grid: Let's move on to nested components

The grid Storyblok already comes with, is an array field called columns, similar to the page component. Currently, its only purpose is to allow a nesting, so let's iterate through that field. As in the teaser before we will access the columns simply using the blok property passed during the include in _layouts/page.html.

Try to replace the content of the grid.html with the code below:

        
      <div class="grid">
  <ul>
  {% for blok in blok.columns %}
    <li>{{ blok.component }}</li>
  {% endfor %}
  </ul>
</div>
    

You should now be able to see the name of the nested components feature, so we also need a file for that particular component in the _includes folder as well.

Nested component "feature"

You can create as many components with as many fields as you like - so you're not limited to those we've just created for you to get started faster. Create more components, nest them and arrange them to beautiful landing pages or enrich existing pages or posts, or create new component as content-type for things like posts, projects and similar.

Let's get back to the feature component for now: Same as with the teaser and grid we can simply create a new file in _includes called feature.

        
      <div class="column">
  <h3>{{ blok.name }}</h3>
</div>
    

and replace the content of the grid.html component to actually include the nested feature components:

        
      <div class="grid">
  {% for blok in blok.columns %}
    {% include {{blok.component}}.html blok=blok %}
  {% endfor %}
</div>
    

Well done! Lets prepare editing!

Now you should have seen the key concept of Storyblok and our nested components, if you only need flat structures simply create a new component as content-type and add your required fields directly.

The next big thing is the way you and your content creators will be able to edit the content of such components. For this purpose, we will now add the Storyblok JavaScript Bridge you may have already seen in the Onboarding (Step 2) of Storyblok.

Simply add the line below in the bottom of your page.html or if you use a custom default.html add it right before the closing </body> tag.

        
      <script src="http://app.storyblok.com/f/storyblok-latest.js" type="text/javascript"></script>
    

Now we need to update our components, in the draft version of the content from the Storyblok API each component ships with a _editable property - containing an HTML comment, place this right before every components.html like we did in the teaser below:

        
      {{blok._editable}}
<div class="teaser">
  <h2>{{blok.headline}}</h2>
</div>
    

The page.html is also a component but acts as a content type and is the "root" of the whole JSON and therefore you can access it's _editable property by adding {{page.story.content._editable}} in front of the <div class="root"> tag..

Rebuild on content change

You can skip this - if you don't want to have an instant preview and prefer to run Jekyll rebuild in the console - this won't be used in production (because you don't need that instant preview) and is not necessary to use the data added in Storyblok. You can also use our Webhooks to trigger a build pipeline.

Since Jekyll generates static files we somehow need to trigger a rebuild after a save or publish event was fired in Storyblok to have an instant preview. To do so we're using the Events of the Storyblok JavaScript Bridge, simply add those lines below the script that you've included before in your page.html.

        
      <script>
storyblok.init()
storyblok.on('change', function() {
  function getAjax(url, success) {
      var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
      xhr.open('GET', url);
      xhr.onreadystatechange = function() {
          if (xhr.readyState>3 && xhr.status==200) success(xhr.responseText);
      };
      xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
      xhr.send();
      return xhr;
  }
  getAjax('/rebuild', function(data){
    console.log(data)
    window.location.reload()
  })
})
</script>
    

You notice that it will call the /rebuild route, which isn't available using a default Jekyll setup. To allow us doing that we're about to add Rack Jekyll to our Gemfile:

        
      gem "rack-jekyll"
    

and execute bundle install. We're now able to create a file in the base folder which is called config.ru with the following content:

        
      require 'rack/jekyll'
require 'jekyll'
require 'yaml'
require 'fileutils'

class Rebuilder
  def self.call(env)
    # Remove the _site folder
    FileUtils.rm_rf("./_site/.", secure: true)

    # Load Jekyll config
    conf = Jekyll.configuration(:source => './', :destination => './_site')

    # Actually rebuild the site
    Jekyll::Site.new(conf).process

    # return a success
    [200, { "Content-Type" => "text/plain" }, ["OK"]]
  end
end

app = Rack::Builder.new do
  map "/rebuild" do
    run Rebuilder
  end

  map "/" do
    run Rack::Jekyll.new(:auto => true, :source => './', :destination => './_site')
  end
end

run app
    

Now we can use Jekyll with a simple Rack app as Rebuilder with the following command:

        
      bundle exec rackup config.ru -p 4000
    

You won't need this in production or wherever you want to publish your pages, this only needs to run to allow an instant preview since you don't want to run a command yourself everytime you've changed something in Storyblok. Of course, you could go with a command every time, or after you've added your changes - but using it like this you will be much faster!

Embed your local environment as preview source

The last step in the onboarding (and to finally allow you to edit your components) simply enter http://127.0.0.1:4000/ in the input at the bottom of the onboarding screen in Storyblok. It will switch to your local address directly - you can change that later in your space settings of course.

Img

I've added some simple styles to the components to make it look better than plain HTML. Simply include those styles to your project and you should see the screen above as you're working in Storyblok.

Summary

Using Storyblok as your CMS with Jekyll is, as you can see, just loading JSON instead of Markdown files. The visual editor can be plugged-in pretty straightforward and only needed for that instant preview. I've really enjoyed using Jekyll because functionality like the includes and layouts are working hand in hand with the idea of components Storyblok allows you to manage. I would love to receive and read your feedback and maybe have a look at some of your implementations. As always you can download the whole source code from Github and comment below.

Author

Dominik Angerer

Dominik Angerer

A web performance specialist and perfectionist. After working for big agencies as a full stack developer he founded Storyblok. He is also an active contributor to the open source community and one of the organizers of Scriptconf and Stahlstadt.js.