/ astro-integrations / How to Use Storyblok with Astro: Complete Guide
astro-integrations 8 min read

How to Use Storyblok with Astro: Complete Guide

Step-by-step guide to integrating Storyblok with your Astro website.

How to Use Storyblok with Astro: Complete Guide

Storyblok is a visual headless CMS that lets content editors work with a real-time preview of the page they are building. Unlike most headless CMS platforms where editors work in a form and hope the output looks right, Storyblok shows them exactly what the page will look like. For teams with non-technical editors, this is a significant advantage.

Astro has an official Storyblok integration, which makes the setup straightforward. You get the visual editor, component-based content, and Astro's static performance in one package.

This guide is verified against @storyblok/astro 9.0.11 and Astro 6.4.2 (checked on 2026-05-29). The package now lives in the Storyblok monorepo at storyblok/monoblok; the standalone storyblok/storyblok-astro repository has been archived.

Prerequisites

  • Node.js 22.12.0 or newer. Astro 6 dropped support for Node 18 and Node 20, so older runtimes will fail the build.
  • An Astro project (npm create astro@latest)
  • A Storyblok account (free tier available with 1 space)

Installation

Install the official Astro Storyblok integration. The Astro CMS guide installs vite alongside it so the config can read environment variables:

npm install @storyblok/astro vite

The integration does not ship an astro add setup entry, so configure astro.config.mjs manually as shown below. If you have seen older guides use npx astro add @storyblok/astro, that path is no longer the documented one.

Configuration

Get your access token from Storyblok under Settings > Access Tokens. Use the "Preview" token for development and the "Public" token for production.

Update your astro.config.mjs. Note that the integration is now a named export (import { storyblok }), not a default export. Older guides that wrote import storyblok from "@storyblok/astro" predate this change and will throw because the package no longer exposes a default export.

// astro.config.mjs
import { defineConfig } from "astro/config";
import { storyblok } from "@storyblok/astro";
import { loadEnv } from "vite";

const env = loadEnv("", process.cwd(), "STORYBLOK");

export default defineConfig({
  integrations: [
    storyblok({
      accessToken: env.STORYBLOK_TOKEN,
      components: {
        page: "storyblok/Page",
        blogPost: "storyblok/BlogPost",
        hero: "storyblok/Hero",
        richtext: "storyblok/RichText",
      },
      apiOptions: {
        region: "eu", // set this for non-EU spaces, e.g. "us", "ap", "ca", "cn"
      },
    }),
  ],
});

The Astro config reads the token with Vite's loadEnv rather than import.meta.env, because integration options are evaluated before Astro's own env is ready. Component values map blok names to files under src/, and the path is given without the .astro extension.

Add your token to .env:

STORYBLOK_TOKEN=your_preview_or_public_token

Basic Usage

Create Astro components that map to your Storyblok content types. Storyblok uses a "blok" system where each content block maps to a component.

---
// src/storyblok/BlogPost.astro
import { storyblokEditable, renderRichText } from "@storyblok/astro";

const { blok } = Astro.props;
const renderedContent = renderRichText(blok.content);
---

<article {...storyblokEditable(blok)}>
  <h1>{blok.title}</h1>
  {blok.image?.filename && (
    <img
      src={`${blok.image.filename}/m/800x450`}
      alt={blok.image.alt}
      loading="lazy"
    />
  )}
  <div set:html={renderedContent} />
</article>

Now create a dynamic page that fetches content from Storyblok:

---
// src/pages/blog/[...slug].astro
import { useStoryblokApi } from "@storyblok/astro";
import StoryblokComponent from "@storyblok/astro/StoryblokComponent.astro";
import BaseLayout from "../../layouts/BaseLayout.astro";

export async function getStaticPaths() {
  const storyblokApi = useStoryblokApi();
  const { data } = await storyblokApi.get("cdn/stories", {
    content_type: "blogPost",
    version: import.meta.env.DEV ? "draft" : "published",
  });

  return data.stories.map((story: any) => ({
    params: { slug: story.slug },
    props: { story },
  }));
}

const { story } = Astro.props;
---

<BaseLayout title={story.name}>
  <StoryblokComponent blok={story.content} />
</BaseLayout>

This is a fully static (SSG) setup, which is what getStaticPaths requires. In Astro 5 and 6, static is the default output mode and output: 'hybrid' was removed. The old hybrid behavior is now built into static, so you no longer set any output flag to mix in a server-rendered route. If you want one page rendered on demand instead of prebuilt, add export const prerender = false to that page and install an adapter; the rest of the site stays static.

Production Tips

  1. Use Storyblok's image service. Append /m/ followed by dimensions to any image URL for on-the-fly resizing. For example, image.filename + "/m/600x400/filters:format(webp)" gives you a resized WebP image without any extra tooling.

  2. Set up the Visual Editor correctly. In Storyblok's settings, configure the preview URL to point to your local dev server (https://localhost:4321). The storyblokEditable helper adds the data attributes needed for the editor to highlight components. The default bridge gives you refresh-on-save highlighting against statically rendered pages, which is enough for most teams.

  • Enable stable Live Preview only if you need instant in-canvas updates. The newer Live Preview feature streams edits into the canvas without a reload, but it requires running Astro in on-demand rendering mode (output: "server" plus an adapter), setting livePreview: true in the integration options, and reading the live story with the getLiveStory and getPayload helpers. The older useStoryblok helper was removed in favor of these. Plain SSG with the default bridge does not support Live Preview, so reach for this only when the editor experience justifies giving up a fully static build.

  • Use the "resolve_relations" parameter. When content references other stories (like author profiles or related posts), pass resolve_relations: "blogPost.author" in your API call to get the full data in one request.

  • Configure webhooks for rebuilds. Under Settings > Webhooks, set up a trigger that fires when stories are published. Point it at your hosting platform's deploy hook.

  • Separate draft and published content. Use version: "draft" during development and version: "published" in production. This lets editors preview changes without affecting the live site.

  • Alternatives to Consider

    • Sanity if you prefer schema-as-code and real-time collaboration without a visual page builder.
    • DatoCMS if you want a simpler form-based CMS with excellent image optimization built in.
    • Contentful if you need a larger integration ecosystem and enterprise features like content approval workflows.

    Common Errors and Fixes

    • storyblok is not a function or a default-import error. The integration is a named export. Use import { storyblok } from "@storyblok/astro", not import storyblok from "@storyblok/astro". The default export was removed, so any guide using the default form is out of date.

    • The build fails on your Node version. Astro 6 requires Node 22.12.0 or newer and no longer supports Node 18 or 20. Upgrade your runtime, or pin a CI image to Node 22 before debugging anything else.

    • output: 'hybrid' is an invalid option. Hybrid output was removed. The static mode is now the default and already includes the ability to mix in server-rendered routes, so remove the output: 'hybrid' line entirely. Mark individual on-demand routes with export const prerender = false.

    • The access token reads as undefined in astro.config.mjs. Integration options are evaluated before Astro's own environment is wired up, so import.meta.env.STORYBLOK_TOKEN can be empty there. Read it with Vite's loadEnv instead, as shown in the config above.

    • Content from a non-EU space returns nothing. The Content Delivery API is region scoped. Set apiOptions.region to match where your space lives (for example "us", "ap", "ca", or "cn"); the EU region is the default when omitted.

    • Live Preview will not stream updates on a static site. Live Preview needs on-demand rendering (output: "server" plus an adapter) and livePreview: true. It also uses getLiveStory and getPayload; the old useStoryblok helper no longer exists. On a default static build you get the bridge's refresh-on-save behavior, not in-canvas streaming.

    • Custom rich text resolvers stop compiling after an upgrade. The underlying @storyblok/richtext v4 dropped the hand-written resolvers option in favor of Tiptap extensions. If you only call renderRichText with no custom resolvers, the upgrade is a clean version bump; if you had custom resolvers, migrate them to Tiptap extensions.

    Official Docs and Examples

    Wrapping Up

    Storyblok and Astro work well together, especially when your content team needs visual editing capabilities. The official integration handles most of the boilerplate, the component mapping system keeps your code organized, and the image service means you do not need a separate media pipeline. If your editors want to see their changes in real time before publishing, this is the CMS to pick.

    Sources

    All versions and facts below were checked on 2026-05-29.