Add a headless CMS to Laravel in 5 minutes


In this short article, we will show you how you can use the headless CMS Storyblok in combination with the PHP Framework for Web Artisans “Laravel”. At the end of this article, you will have a Laravel Application which renders components according to the data of the API of Storyblok.

Storyblok integrates in laravel

Let me start with a short explanation of Storyblok first: Storyblok is a headless CMS in which you can create unlimited components and content-types .

You can then call the content delivery api, receive that JSON and render your components accordingly. During this article we will use the JSON you will receive after creating a new space.


I’m sure that most of you are already familiar with Laravel and it’s basics - if not I would suggest you start with the Installation of Laravel. The Laravel framework has a few system requirements:

Start a new Laravel project

You can add Storyblok to existing projects as well - for simplicity we will show how to add Storyblok to a completely fresh project - so a beginner to the world of Laravel can use Storyblok as their CMS as well because it’s API-based and only returns data for your application. Execute the following command so you’ve got a freshly created project ready to start with:

laravel new larablok

You can simply run your fresh application after executing:

composer install && php artisan serve

You can read more about the laravel setup in their documentation mentioned above.

Install the Storyblok PHP Client

Storyblok already provides a PHP client - so we won’t have to think about how we’re doing the API requests and receive data from the content delivery API – all we have to do is:

composer require storyblok/php-client

This will add the \Storyblok\Client to your composer.json.

Let’s load our Story

In the routes/web.php, we will initialize the Storyblok Client and directly load the Story with the slug "home" as default– and a route parameter to load a story according to the slug which was received as an optional parameter.

The string we passed to the client is the previewToken for our test space which provides the data till we will exchange that to your very own in a later step.

Route::get('/{slug?}', function ($slug = 'home') {
  $storyblok = new \Storyblok\Client('akYA0RB4BzCPUoRfjIvUdQtt');
  $data = $storyblok->getStoryBySlug($slug)->getStoryContent();
  return view('index', ['story' => (object)$data['story']]);

The above code tries to render the index.blade.php, since this is missing we will have to create it first.

Create index.blade.php

In your resources/views/index.blade.php:

<!doctype html>
<html lang="{{ config('app.locale') }}">
    <title>{{ $story->name }}</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" />
    <meta http-equiv="X-UA-Compatible" content="IE=11"/>
    <meta name="generator" content="storyblok">
    <meta name="cms" content="">
    <link rel="icon" type="image/png" href="//" sizes="32x32">
    <link rel="stylesheet" href="{{ url('css/app.css') }}" media="all" />

  @include('components/' . $story->content['component'], ['blok' => $story->content])
  <script type="text/javascript" src="{{ url('js/app.js') }}"></script>

  <script type="text/javascript" src="//"></script>
  <script type="text/javascript">
    storyblok.on('change', function() {

Let’s focus on one line by now:

@include('components/' . $story->content['component'], ['blok' => $story->content])

The code above uses the build in functionality of the blade templating engine to include partials with a parameter passed to render different components according to your content.

The first component in the demo content is called “page”, we call those first components: content types.

Create our first component

To still have some kind of overview in our *.blade.php files I’ve created a views/components folder so we know which components are used for those includes.

The page component/content type) only has one array as property – body of the type “blocks” which is a holder for nested components.

Create the page.blade.php

Add the following resource: resources/views/components/page.blade.php.

@foreach ($blok['body'] as $blok)
  @include('components/' . $blok['component'], ['blok' => $blok])

The next component in our demo content is the teaser component, which is nested in that page content type.

Let’s create the teaser.blade.php

Add the next component resources/views/components/teaser.blade.php.

{!! isset($blok['_editable']) ? $blok['_editable'] : '' !!}
<div class="teaser">
      You can access every attribute you
      define in the schema in the blok variable
        You can create new components like this - to create your own set of components.

Let’s create the grid.blade.php component

The demo content already provides the content for that component, similar to the teaser component we will have to create a file for this as well.

{!! isset($blok['_editable']) ? $blok['_editable'] : '' !!}
<div class="grid">
  @foreach(array_get($blok, 'columns', []) as $blok)
      @include('components/' . $blok['component'], ['blok' => $blok])

Last component: feature.blade.php

You can see above that the grid component also only contains one array property which includes another component, called “feature”.

{!! isset($blok['_editable']) ? $blok['_editable'] : '' !!}
<div class="feature">
  {{ $blok['name'] }}

The Storyblok JavaScript Bridge

{!! isset($blok['_editable']) ? $blok['_editable'] : '' !!}

This line of code will output the text included in the _editable property of an Storyblok component. If your application will be opened in the preview mode or Storyblok Editor, we need some kind of match to your website so we can identify a component.

The content of the _editable property is actually nothing more than a simple HTML comment - with the Storyblok script we included in the index.blade.php we can enable frontend editing without touching your actual HTML. Have a look at the JavaScript Bridge documentation for more information and even events.

If you’re running on an Nginx with server side includes on, you can use this Github Gist to manually parse the HTML comments and apply the attributes accordingly.

Configuring Storyblok

Now that you have successfully integrated Storyblok in your project let’s create a “Story” in your own Storyblok space.

  1. Go to!/signup and do the signup
  2. Create a new Space.

Exchange the preview token

Copy your preview token to receive the draft version of your content. The preview token is a read only token and can be found in the space dashboard. Insert your token in “routes/web.php”.

Route::get('/{slug?}', function ($slug = 'home') {
  $storyblok = new \Storyblok\Client('YOUR_ACCESS_TOKEN');
  $data = $storyblok->getStoryBySlug($slug)->getStoryContent();
  return view('index', ['story' => (object)$data['story']]);

Preview token

After adding your own token from your Space to your project - we will have to also tell Storyblok where to find our dev environment. You can either add it at the bottom of the on-boarding in your home content entry, you can change that at any time in the Space Settings.

Storyblok on boarding

Well done!

Try out to insert a text or add new components, after one click on “Save” your component should be updated with the changed content. You can add some styling to the HTML and receive something like we did below.

You can now create as many components, and content types as you want, build new layouts with nested components - or go flat with content types like post, project, and product.



You can create as many components and with as many fields as you want. You can even nest them as deep as you want them to. I would suggest you to read the Component Terminology before you start creating your own components.

Component NameComponent Use
pageholder for all nested components - only has a property of the typ "blocks".
teasercontains the property "headline" so the component teaser can be edited in the SideBySide Editor.
gridholder of feature components
featurea simple component including one text field

If you want to know how to create the space to this article you can just follow our youtube tutorial for this setup. You can also find the whole package we created during this tutorial on github.

More to read...

About the 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 Stahlstadt.js.