Storyblok Visual Editor Integration
This topic, within Platform Integration Deep Dives, covers integrating Storyblok, a hosted headless CMS built around nestable component blocks and a Visual Editor that shows the real frontend while editors work. It explains how stories and blocks map to frontend components, how the Visual Editor and its bridge script connect to the site, how draft and published versions and the cache version parameter shape fetching and caching, and how relations, webhooks, localization and generated types fit together.
Storyblok’s model differs from form-first CMSs in one important way: a page is a tree of blocks that editors compose, reorder and nest, and each block type corresponds to a component in the frontend. The frontend is therefore not just a consumer of data but the editor’s canvas. That makes the integration more rewarding and more demanding at the same time: rewarding because editors see exactly what they build, demanding because every component must render drafts correctly, mark itself as editable and cope with blocks it has never seen.
Integration Contract
A Storyblok integration rests on a few decisions. Stories and blocks: each page is a story whose content is a root block of a content type such as page or article, containing nested blocks. Every block has a component name and a _uid, and the frontend keeps a registry that maps component names to components. Versions: the Content Delivery API serves version=published to readers and version=draft to the Visual Editor and previews. Tokens: a public access token reads published content and may be used in the browser; a preview token reads drafts and stays on the server. Cache version: requests carry a cv parameter, the space’s current cache version, so CDN caches are invalidated when content is published. Region: each space lives in a region, and the API host must match it. Events: webhooks on publish, unpublish and delete, signed with a secret, call one revalidation handler.
# .env: Storyblok integration
STORYBLOK_REGION=eu # eu, us, ap, ca or cn; selects the API host
STORYBLOK_PUBLIC_TOKEN=public_access_token # published content only; may reach the browser
STORYBLOK_PREVIEW_TOKEN=preview_access_token # drafts; server only
STORYBLOK_WEBHOOK_SECRET=secret_set_on_the_webhook
STORYBLOK_MANAGEMENT_TOKEN=personal_access_token_for_ci # CLI and Management API, never deployed
The Management API, used for schema exports, migrations and type generation, is a separate API with its own tokens; it never belongs in the frontend’s runtime environment.
Core Implementation Pattern
The core of a Storyblok frontend is a small set of pieces: a client configured for the region and token, a fetch helper that picks the version, and a component registry with a generic block renderer. With the official SDKs, such as @storyblok/react, the pattern looks like this:
// lib/storyblok.ts
import { storyblokInit, apiPlugin } from "@storyblok/react/rsc";
import Page from "@/components/blocks/Page";
import Hero from "@/components/blocks/Hero";
import Teaser from "@/components/blocks/Teaser";
import Grid from "@/components/blocks/Grid";
export const getStoryblokApi = storyblokInit({
accessToken: process.env.STORYBLOK_PREVIEW_TOKEN, // server-side client; version decides what is returned
apiOptions: { region: process.env.STORYBLOK_REGION },
use: [apiPlugin],
components: { page: Page, hero: Hero, teaser: Teaser, grid: Grid },
});
export async function fetchStory(slug: string, draft: boolean) {
const api = getStoryblokApi();
const { data } = await api.get(`cdn/stories/${slug}`, {
version: draft ? "draft" : "published",
resolve_relations: ["article.author", "featured-articles.articles"],
resolve_links: "url",
}, draft ? { cache: "no-store" } : { next: { tags: [`story:${slug}`] } });
return data.story;
}
// components/blocks/Teaser.tsx
import { storyblokEditable } from "@storyblok/react/rsc";
export default function Teaser({ blok }: { blok: { _uid: string; headline: string; text?: string } }) {
return (
<section {...storyblokEditable(blok)} className="teaser">
<h2>{blok.headline}</h2>
{blok.text && <p>{blok.text}</p>}
</section>
);
}
storyblokEditable adds attributes that the Visual Editor uses to outline the block and open it on click. In the published version, blocks carry no _editable data, so the helper adds nothing. A generic StoryblokComponent, or your own renderer, looks up each block’s component name in the registry and renders the matching component; unknown names should render nothing in production and a visible placeholder in the editor, so a new block type added in Storyblok before the frontend supports it never breaks a page.
Why this shape? The registry makes the mapping between Storyblok’s schema and the codebase explicit and reviewable. The single fetch helper guarantees that every request, including those for navigation and footers, respects draft mode. And rendering unknown blocks defensively decouples editors’ schema work from frontend releases, as described in content modeling best practices.
Caching & Invalidation Strategy
Storyblok’s Content Delivery API is served through a CDN, and its caching model revolves around the cache version. Every published change increases the space’s cache version; requests with a cv parameter are cached under that version, so a request with the new cv bypasses stale entries. The official clients fetch the current version from the space endpoint and append it automatically. Draft requests are never cached by the CDN and count against the uncached rate limits, so they belong in preview only.
On the frontend, cache published responses with tags per story and per list, and revalidate on webhooks. Storyblok signs webhook requests when a secret is set, with an HMAC of the raw body in the webhook-signature header; verify it before revalidating anything. For static builds, trigger a rebuild from the webhook with debouncing. Keep the public token’s responses cacheable at your own CDN too, since the content is public, and send Cache-Control from the frontend that reflects your revalidation model. Asset URLs from Storyblok’s asset CDN are immutable per upload, so they can be cached for a year; transformations through the image service create new URLs.
Schema & Content Modeling Considerations
Storyblok distinguishes content type blocks, which are the root of a story, nestable blocks, which live inside fields of type blocks, and universal blocks, which can be both. Keep the set of nestable blocks deliberate: every block is a component to build, test and maintain, and a library that grows without review ends up with five kinds of teaser. Restrict which blocks each blocks field allows, so editors cannot put a full-width hero inside a sidebar.
Relations between stories use single or multi-option fields with stories as the source. By default the API returns only the related story’s uuid; resolve_relations with the block and field names inlines related stories into the response, and resolve_links resolves link fields to stories or URLs. Resolved relations increase response size, and each request can resolve only a limited number of relations, so resolve what the page renders and nothing more; the guide on resolving relations and links covers the details.
Localization comes in two forms. Field-level translation marks fields as translatable and fetches a language with the language parameter; folder-level translation keeps a separate folder tree per locale, useful when locales have different structures. Field-level is simpler for sites whose locales mirror each other; folder-level suits sites with independent regional content. Either way, links resolved with resolve_links=url return full slugs that the frontend maps to routes, so route mapping should live in one function.
Rich text fields return a document structure, not HTML. Render it with Storyblok’s rich text renderer and map embedded blocks to the same component registry, so blocks inside rich text behave like blocks anywhere else.
Preview & Draft Workflow
Storyblok’s Visual Editor loads the site’s preview URL in an iframe and adds query parameters such as _storyblok with the story id. The frontend recognises the editor, enables its draft mode and fetches version=draft with the preview token. The Storyblok bridge, a small script loaded only in the editor, reports changes as editors type; the frontend either re-renders the story from the bridge’s input event or refetches the draft. The broader patterns are covered in preview and draft workflow patterns; the specifics are in real-time visual editing with the Storyblok bridge, with the security boundary around it in securing Visual Editor previews.
Three rules keep preview safe. The preview token never reaches the browser outside the editor context; draft fetching happens on the server behind a draft mode that only a validated request can enable. The bridge script loads only in draft mode, never for readers. And draft responses carry noindex and are excluded from shared caches. Storyblok’s editor also verifies that the frontend is framed only by the Storyblok app when you validate the _storyblok_tk parameters, which include a timestamp and a token derived from the preview token; checking them is a sensible extra gate before enabling draft mode.
Error Handling & Resilience
Storyblok’s CDN is highly available, but the frontend still needs a plan for failures. Distinguish between a story that does not exist, which the API reports with a 404 and the frontend turns into its own not-found page, and a failed request, which should be retried with backoff and then served from the last good cache. Uncached requests are rate-limited; a 429 response should be retried after a short delay, and a build that fetches thousands of stories should use the stories list endpoint with pagination and moderate concurrency rather than one request per story.
Blocks are the other failure surface. A block with missing required fields, which can happen when fields are added after content was created, should render a reduced version or nothing, never throw. Wrap each block in an error boundary so one broken block does not take down the page, and log the block’s component name and _uid so editors can find it. When the API returns a block type the registry does not know, log it as a warning; it usually means a schema change reached production before the frontend support for it.
Testing & Observability
Test the block registry and components with fixture stories captured from a development space, covering each block type with minimal and maximal content. A contract test in CI fetches a handful of real stories from a staging space, including resolved relations, and asserts that required fields are present and that every block type in them has a registered component. The broader approach is in automated testing for headless integrations.
For observability, log each Storyblok request’s endpoint, version, cache version, status and duration, and count unknown block types and rate-limited responses. Log webhook deliveries with the story’s full slug and the tags revalidated. A dashboard with draft request volume shows how much the Visual Editor costs in API usage, and a spike in unknown blocks catches schema changes that outran the frontend.
// lib/storyblok-log.ts
export function logStoryblok(e: { endpoint: string; version: "draft" | "published"; cv?: number; status: number; ms: number }) {
console.log(JSON.stringify({ kind: "storyblok_request", ...e }));
}
Generated Types
Storyblok’s CLI can pull the space’s component schemas and generate TypeScript types for every block. Commit the generated file and regenerate it in CI from the staging space, so a changed field becomes a type error in the component that renders it. Types describe the schema, not the content: fields that editors may leave empty are optional in the generated types, and components should handle that. The workflow is described in generating TypeScript types for Storyblok components.
Worked Example
A furniture retailer moved its marketing site from a page builder to Storyblok with a Next.js frontend. The team defined 24 nestable blocks, a registry with a fallback for unknown blocks, a single fetch helper for draft and published versions, signed webhooks revalidating tags per story, and generated types. Editors built landing pages in the Visual Editor, seeing changes as they typed, and published without developer involvement. The median time from brief to live landing page fell from nine working days to two, and the frontend’s origin traffic stayed low because published content was served from caches invalidated only on publish.
Designing the Block Library
The block library is the product that editors use every day, so treat it like one. Start from the page designs and identify recurring sections, then define blocks at the level of those sections rather than at the level of individual elements: a “feature grid” block rather than separate blocks for icon, heading and text. Give each block a small number of meaningful options, such as a theme or a layout variant, instead of free styling fields that let editors break the design. Write a short description and a preview image for each block in Storyblok, so editors can choose with confidence. Review new blocks in the same way as new components, with design, accessibility and performance checks, and retire blocks that are no longer used by migrating their content to current ones.
Workflow, Releases and Permissions
Storyblok offers workflow stages, releases for publishing sets of changes together, and pipelines for moving content between stages such as staging and production spaces or branches. Releases fit campaign launches well: prepare all stories for a launch in a release, preview it, and publish it at once. The frontend can preview a release by passing its id with the draft request, so reviewers see the combined state. Keep permissions narrow: editors who build pages rarely need to change block schemas, and schema changes should come from developers, reviewed like code, because they change what the frontend must render.
Asset Handling
Storyblok’s asset CDN serves uploaded files and resizes and converts images through URL parameters, for example requesting a width and a modern format. Build image URLs in one helper that adds width, format and quality, and use it for srcset so browsers load the right size. Require alt text in the asset fields for images that carry meaning, and pass focal points from the asset to the crop parameters so important parts of images survive cropping. Because transformed URLs change when parameters change, they can be cached for a long time at every layer.
Ownership
A Storyblok integration has three owners. Developers own the block registry, generated types, fetch helper and webhook handler, and review every schema change. Content designers or a design system team own the block library’s purpose and options. Editors own stories and releases. Write down who may change the schema in which space, and route schema changes through a pull request that includes the component work, so the library in Storyblok and the components in code never drift apart.
Frequently Asked Questions
Is the Visual Editor required?
No. Storyblok works as a form-based headless CMS too, but most of its value for page-building teams comes from the Visual Editor, and the integration effort is modest.
Can the public token be exposed in the browser?
Yes, it can only read published content. The preview token must stay on the server.
Do we need to handle the cache version ourselves?
With the official clients, no; they fetch and append it. With custom HTTP clients, fetch the space’s version and add cv to requests.
REST or GraphQL?
Storyblok offers both. The REST Content Delivery API with the official SDKs is the most common and best-documented path for block-based pages.
How do we handle a block the frontend does not know yet?
Render nothing for readers and a clear placeholder in the editor, and log the block type, so schema work never breaks published pages.
Which region should we choose?
The one closest to your content team and data residency requirements. The frontend must use the matching API host.
Can several sites share one space?
Yes, with a folder per site and site-specific settings, or with separate spaces when sites need different block libraries and permissions.