The three words you need to understand Storyblok

Every website needs two different things to exist: the code that decides what a page looks like, and the actual words, images and details that fill it in. For a long time, those two things lived in the same place. A developer wrote the homepage's copy directly into the page's code, and if marketing wanted to change one sentence, they had to ask a developer to do it, then wait for a deploy. Nobody enjoyed that meeting.
Headless CMS platforms exist to split that apart. A traditional CMS, like classic WordPress, comes bundled with its own templates and its own rendering, the "head" that decides how content actually looks on a page, hence the name. A headless CMS strips that head off entirely: it only stores and manages content, and leaves rendering to whatever app asks for it. Storyblok is one of these.
That split is what gives editors a place to write and manage content on their own, without touching code, while developers keep full control over how that content actually renders. Storyblok's job sits in the middle: it stores structured content and hands it back through an API, a way for other software to ask for and receive that content, whenever a page or app needs it. It renders nothing itself, your app does that part, fetching what Storyblok stored and turning it into an actual page.
I didn't know any of this before I joined my current company. I already knew CMS platforms in general, I'd worked with a few over the years, but headless specifically, and Storyblok specifically, is something I only picked up on the job, over the last two years of actually working with it day to day. This is how I'd explain it to someone starting on day one.
Here is where that actually sits in the flow of making a page:
Storyblok only owns the second box. Everything on either side of it, writing, fetching, rendering, is someone else's job.
That fourth box says Next.js because that is what I actually work in, but nothing about Storyblok cares which framework is doing the fetching and rendering. It ships an official quickstart for most of the major ones, all scaffolded with the same CLI command, just a different --template:
- Next.js:
storyblok create --template nextjs
Everything in this post, spaces, stories, blocks, the API in the middle, works the same regardless of which framework ends up rendering the page.
Three terms make everything else about Storyblok make sense, and once they click, most of what looks confusing in the docs stops being confusing:
- A space is one Storyblok project: the equivalent of one whole codebase for one site. It holds that project's stories, blocks, assets and datasources, plus the people who have access to it.
- A story is one piece of content, the equivalent of one page or route in a website:
/about,/blog/my-post,/team/jane-doe. Every story has a slug (the readable part of its URL, likeabout-us) and acontentobject (the actual fields and values an editor filled in). - A block (also called a component) is a named set of fields, the equivalent of a component in your codebase:
Hero.tsx,Card.tsx. This is the unit of schema, and the thing you spend most of your time defining.herois a block.feature_cardis a block. In code, though, you will see it spelledblok, no c, it's the actual prop name Storyblok's SDKs pass into your components:const Article = ({ blok }) => ....
Most spaces are built entirely through the UI, clicking to create each block. Some teams, mine included, manage that same content model as code instead. Here is what that looks like: one block per file, all of them bundled into a single schema.ts.
my-nextjs-app/
├── app/
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── Hero.tsx
│ └── FeatureCard.tsx
├── storyblok/
│ ├── blocks/
│ │ ├── blog-post.ts
│ │ ├── hero.ts
│ │ └── feature-card.ts
│ └── schema.ts
└── package.json
components/: the React components that render on the page,Hero.tsx,FeatureCard.tsx.storyblok/blocks/: the schema for those same things, one file per block.storyblok/schema.ts: bundles all of those block files together.
Two files, almost the same name, doing very different jobs, exactly the kind of thing that trips people up when they're skimming at 2am:
hero.ts: defines what fields aherois allowed to have, e.g. a heading, an image, a link.Hero.tsx: decides how those fields actually render.
That's the whole idea: every block the project uses lives in storyblok/blocks/, one file each, and schema.ts bundles them into one thing you can push. These files define what shape a story's content is allowed to take. They don't hold any actual content, the real pages editors publish live in Storyblok itself, not in your repo.
One more distinction worth knowing: blocks come in two main kinds, plus a blended third.
- A content type block, like
blog_post, is the fixed skeleton for a kind of page, markedis_root: true. You build it once. - A nestable block, like
heroorfeature_card, is the same idea as all the smaller components you'd normally reach for to build a page, a hero section, a card grid, a testimonial. It only ever gets placed inside another block, the wayheromight sit insideblog_post'sbodyfield, and it can never be a page on its own. Editors assemble a bunch of them to fill out each content type block differently, the same way you'd compose a page out of components in code. - A universal block can be both. Storyblok's own example is a CTA, useful for a component that needs to work as a standalone page and as a piece inside one.
blog_post — "My First Post"
That's what building a page actually looks like: one Hero at the top, then as many Feature Cards below it as the post needs, the same nestable block reused three times inside one content type block.
Put it all together, and that's what Storyblok actually is: a place that stores your content, shaped however your schema says it should be, and nothing more. A space holds everything for one project. A story is one real page or piece of content inside it. A block is the shape that content is allowed to take, a content type block if it can stand at the top as a page, nestable if it only ever lives inside something bigger. Storyblok never decides what any of that looks like on screen, your own app still does that part. It just makes sure the content going into it is structured the same way every time.
One Storyblok project, e.g. my-nextjs-app
One real page, e.g. /blog/my-post
blog_post — the skeleton for this page
hero, feature_card — pieces inside it
If you want to see this content model written as actual code, defineBlock, is_root, field types and all, I wrote a separate post about it: What I learned building a Storyblok schema.
Get notified when I post
No spam, just a note when something new is up.