Skip to main content
Technology & EngineeringCms Services306 lines

Storyblok

Build with Storyblok as a visual headless CMS. Use this skill when the project needs a component-based CMS with a real-time visual editor, nested content blocks, localization, asset management, or framework integration (Next.js, Nuxt, Astro). Covers space setup, content modeling with bloks, the Storyblok Bridge, and webhook-driven revalidation.

Quick Summary26 lines
You are an expert in integrating Storyblok as a headless CMS. Storyblok is a
component-based headless CMS with a real-time visual editor that lets content
editors compose pages from reusable blocks while developers maintain full
control over rendering.

## Key Points

- Use `storyblokEditable(blok)` on every component's root element so the visual editor can highlight and click into it
- Resolve relations at query time with `resolve_relations` instead of making additional API calls
- Use the Storyblok image service transforms (`/m/WxH/filters:format(webp)`) instead of downloading and re-processing images
- Define a `catch-all` route (`[...slug]`) to let Storyblok's folder structure drive your URL hierarchy
- Keep bloks granular and composable — a `Hero` blok should accept a `buttons` field of nested `Button` bloks rather than hardcoding button fields
- Mixing up `draft` and `published` versions — always use `draft` only behind preview mode; public pages must use `published`
- Forgetting to register components in `storyblokInit` — unregistered bloks silently render nothing, which is hard to debug
- Not validating webhook signatures — always verify the `webhook-signature` header before revalidating
- Using the preview token in client-side code — the preview token exposes draft content; use the public token for client-side requests

## Quick Example

```env
STORYBLOK_API_TOKEN=your-preview-token
STORYBLOK_PUBLIC_TOKEN=your-public-token
STORYBLOK_WEBHOOK_SECRET=your-webhook-secret
```
skilldb get cms-services-skills/storyblokFull skill: 306 lines
Paste into your CLAUDE.md or agent config

Storyblok — CMS Integration

You are an expert in integrating Storyblok as a headless CMS. Storyblok is a component-based headless CMS with a real-time visual editor that lets content editors compose pages from reusable blocks while developers maintain full control over rendering.

Core Philosophy

Overview

Storyblok treats every piece of content as a story composed of nestable bloks (components). Its visual editor overlays your actual frontend so editors see exactly what they are changing. Content is delivered via a REST or GraphQL Content Delivery API, with draft content available through a separate preview token.

Setup & Configuration

Install

# Next.js project
npm install @storyblok/react

# Nuxt project
npx nuxi module add @storyblok/nuxt

# Astro project
npx astro add @storyblok/astro

Environment variables

STORYBLOK_API_TOKEN=your-preview-token
STORYBLOK_PUBLIC_TOKEN=your-public-token
STORYBLOK_WEBHOOK_SECRET=your-webhook-secret

Client initialization (Next.js)

// lib/storyblok.ts
import { storyblokInit, apiPlugin } from '@storyblok/react/rsc';
import Page from '@/components/bloks/Page';
import Teaser from '@/components/bloks/Teaser';
import Grid from '@/components/bloks/Grid';
import Feature from '@/components/bloks/Feature';

export const getStoryblokApi = storyblokInit({
  accessToken: process.env.STORYBLOK_API_TOKEN,
  use: [apiPlugin],
  components: {
    page: Page,
    teaser: Teaser,
    grid: Grid,
    feature: Feature,
  },
});

Core Patterns

Content modeling with bloks

Bloks are Storyblok's reusable content components. Define them in the Storyblok UI, then create matching React components.

// components/bloks/Feature.tsx
import { storyblokEditable, type SbBlokData } from '@storyblok/react/rsc';

interface FeatureBlok extends SbBlokData {
  name: string;
  description: string;
  icon: { filename: string; alt: string };
}

export default function Feature({ blok }: { blok: FeatureBlok }) {
  return (
    <div {...storyblokEditable(blok)} className="feature">
      {blok.icon?.filename && (
        <img
          src={`${blok.icon.filename}/m/64x64`}
          alt={blok.icon.alt || ''}
        />
      )}
      <h3>{blok.name}</h3>
      <p>{blok.description}</p>
    </div>
  );
}

Fetching stories

// lib/storyblok.fetch.ts
import StoryblokClient from 'storyblok-js-client';

const client = new StoryblokClient({
  accessToken: process.env.STORYBLOK_API_TOKEN,
});

export async function getStory(slug: string, preview = false) {
  const { data } = await client.get(`cdn/stories/${slug}`, {
    version: preview ? 'draft' : 'published',
    resolve_relations: 'post.author,post.categories',
  });
  return data.story;
}

export async function getStories(params: {
  startsWith?: string;
  perPage?: number;
  page?: number;
}) {
  const { data, headers } = await client.get('cdn/stories', {
    version: 'published',
    starts_with: params.startsWith,
    per_page: params.perPage ?? 25,
    page: params.page ?? 1,
    is_startpage: false,
  });
  return {
    stories: data.stories,
    total: parseInt(headers.total, 10),
  };
}

Rendering nested bloks

// components/bloks/Page.tsx
import { StoryblokComponent, storyblokEditable, type SbBlokData } from '@storyblok/react/rsc';

interface PageBlok extends SbBlokData {
  body: SbBlokData[];
}

export default function Page({ blok }: { blok: PageBlok }) {
  return (
    <main {...storyblokEditable(blok)}>
      {blok.body?.map((nestedBlok) => (
        <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
      ))}
    </main>
  );
}

Data fetching in Next.js App Router

// app/[...slug]/page.tsx
import { getStory } from '@/lib/storyblok.fetch';
import { StoryblokComponent } from '@storyblok/react/rsc';

export default async function CatchAllPage({
  params,
}: {
  params: { slug: string[] };
}) {
  const slug = params.slug?.join('/') || 'home';
  const story = await getStory(slug);

  return <StoryblokComponent blok={story.content} />;
}

export async function generateStaticParams() {
  const { stories } = await getStories({ perPage: 100 });
  return stories.map((story: any) => ({
    slug: story.full_slug.split('/'),
  }));
}

Visual editor bridge

// components/StoryblokBridgeLoader.tsx
'use client';

import { useStoryblokBridge } from '@storyblok/react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';

export default function StoryblokBridgeLoader({ storyId }: { storyId: number }) {
  const router = useRouter();
  const [story, setStory] = useState<any>(null);

  useStoryblokBridge(storyId, (newStory) => {
    setStory(newStory);
    router.refresh();
  });

  return null;
}

Image optimization with Storyblok image service

// lib/storyblok.image.ts
export function optimizeImage(
  url: string,
  options: {
    width?: number;
    height?: number;
    quality?: number;
    format?: 'webp' | 'avif' | 'png' | 'jpeg';
    fit?: 'crop' | 'contain' | 'cover';
    focal?: string;
  } = {}
): string {
  if (!url) return '';
  const { width, height, quality = 80, format = 'webp', fit, focal } = options;
  const transforms: string[] = [];

  if (width || height) {
    transforms.push(`${width || 0}x${height || 0}`);
  }
  if (fit === 'crop' && focal) {
    transforms.push(`filters:focal(${focal})`);
  }
  if (quality) {
    transforms.push(`filters:quality(${quality})`);
  }
  if (format) {
    transforms.push(`filters:format(${format})`);
  }

  return `${url}/m/${transforms.join('/')}`;
}

Webhook-triggered revalidation

// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import crypto from 'crypto';

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get('webhook-signature');

  const expectedSig = crypto
    .createHmac('sha1', process.env.STORYBLOK_WEBHOOK_SECRET!)
    .update(body)
    .digest('hex');

  if (signature !== expectedSig) {
    return new Response('Invalid signature', { status: 401 });
  }

  const payload = JSON.parse(body);
  const slug = payload.full_slug || payload.story?.full_slug;

  if (slug) {
    revalidatePath(`/${slug}`);
  } else {
    revalidatePath('/', 'layout');
  }

  return Response.json({ revalidated: true, now: Date.now() });
}

Best Practices

  • Use storyblokEditable(blok) on every component's root element so the visual editor can highlight and click into it
  • Resolve relations at query time with resolve_relations instead of making additional API calls
  • Use the Storyblok image service transforms (/m/WxH/filters:format(webp)) instead of downloading and re-processing images
  • Define a catch-all route ([...slug]) to let Storyblok's folder structure drive your URL hierarchy
  • Keep bloks granular and composable — a Hero blok should accept a buttons field of nested Button bloks rather than hardcoding button fields

Common Pitfalls

  • Mixing up draft and published versions — always use draft only behind preview mode; public pages must use published
  • Forgetting to register components in storyblokInit — unregistered bloks silently render nothing, which is hard to debug
  • Not validating webhook signatures — always verify the webhook-signature header before revalidating
  • Using the preview token in client-side code — the preview token exposes draft content; use the public token for client-side requests

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add cms-services-skills

Get CLI access →

Related Skills

Strapi

Build with Strapi for self-hosted headless CMS. Use this skill when the project needs Strapi integration for content types, REST or GraphQL APIs, custom controllers, plugin development, admin panel customization, or authentication. Covers Strapi v5 patterns and deployment.

Cms Services377L

Tina

Tina is an open-source, Git-backed headless CMS that enables developers to build powerful content-editing experiences directly within their code. Use Tina when you need a local-first CMS for modern web applications, integrating content directly into your Git workflow and leveraging GraphQL for data fetching.

Cms Services251L

Builder Io

Builder.io is a Visual Headless CMS and API that empowers developers to integrate a drag-and-drop page builder and content management system into any web application. Use this skill when you need a powerful visual editor for marketing and content teams, A/B testing, personalization, and a flexible content API for dynamic web experiences.

Cms Services138L

Caisy

caisy is a headless CMS designed for speed and scalability, empowering developers to deliver exceptional digital experiences. Use caisy when you need a performant, API-first content platform with strong developer tools, a focus on speed, and a GraphQL-native approach.

Cms Services249L

Contentful

Build with Contentful for headless content management. Use this skill when the project needs Contentful integration for content models, REST or GraphQL APIs, rich text rendering, localization, webhooks, environment management, or Next.js SDK usage. Covers the full Contentful developer workflow.

Cms Services359L

Cosmic

Integrate Cosmic as your headless content management system, providing a flexible and scalable solution for managing digital content across websites, mobile apps, and other digital experiences. Use Cosmic when you need a powerful API-first CMS with robust media management, localization, and customizable content models.

Cms Services181L