npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

storyblok-nextjs

v1.9.0

Published

Next.js integration for Storyblok

Readme

storyblok-nextjs

Modern Next.js integration for Storyblok

Features

  • 🚀 React Server Components Support: Built for Next.js App Router
  • 🔄 Live Preview: Real-time preview with automatic updates via Storyblok Bridge
  • 🗄️ Smart Caching: Automatic cache invalidation using Next.js cache tags
  • 🖼️ Image Optimization: Seamlessly use next/image with Stroyblok assets

Quick Start

The easiest way to get started is by using this command:

npx create-next-app@latest --example https://github.com/lionizers/storyblok-nextjs-example

Create a .env.local in the root directory of the newly created project and add the following line:

STORYBLOK_PREVIEW_TOKEN=<YOUR-PREVIEW-TOKEN>

Manual Setup

Alternatively, you can follow this step-by-step guide to set things up manually. Start by installing the package:

npm install storyblok-nextjs

1. Create React components for your blocks

Let's start with a simple component that matches the teaser block type as it is present in Storyblok's Demo Space:

// /blocks/Teaser.tsx

type Props = {
  title: string;
};
export function Teaser({ title }: Props) {
  return (
    <div>
      <h2>{title}</h2>
    </div>
  );
}

Obviously, a teaser with just a title is not very helpful. No worries, we'll revisit this later. Note that we don't have to do anything special to make the component compatible with Storyblok's visual editor. The Render components, we'll create in the next step will take care of this.

2. Create Render components

In order to render our blocks, we need some helpers – the so called Render components:

  • <Render.List /> – Renders a list of blocks.
  • <Render.One /> – Renders a single block.
  • <Render.RichText /> – Renders a RichText document which may contain blocks.
  • <Render.LivePreview /> – Renders a story, enabling live updates via the Storyblok Bridge.

We can create all these components by calling createRenderComponents(), passing all our previously created blocks. Make sure to use the same names as the technical name in your Block Library.

// /blocks/index.ts

import { createRenderComponents } from "storyblok-nextjs";
import feature from "./Feature";
import grid from "./Grid";
import teaser from "./Teaser";
import page from "./Page";

export const Render = createRenderComponents({
  feature,
  grid,
  teaser,
  page,
});

3. Use the Render components

Let's create another block for a type that also comes with Storyblok's Demo Space: a grid. It has a field called columns with the type Blocks. We can type this properly by importing the Block type from storyblok-nextjs.

We'll then import the Render components we created in the previous step and use <Render.List /> to render the grid columns:

// /blocks/Grid.tsx

import type { Block } from "storyblok-nextjs";
import { Render } from "@/blocks";

type Props = {
  columns: Block[];
};

export default function Grid({ columns }: Props) {
  return (
    <div className="grid grid-cols-3">
      <Render.List blocks={columns} />
    </div>
  );
}

3.1 Rendering rich text

The <Render.RichText /> component can be used to render rich text fields with nested blocks. As an example, we'll add a rich text field to the teaser block:

// /blocks/Teaser.tsx

import type { RichText } from "storyblok-nextjs";
import { Render } from "@/blocks";

type Props = {
  headline: string;
  text?: RichText;
};

export default function Teaser({ headline, text }: Props) {
  return (
    <div>
      <h2 className="text-2xl mb-10">{headline}</h2>
      <Render.RichText text={text} />
    </div>
  );
}

3.2 Customizing Rich Text Rendering Options

You can customize the Rich Text rendering options by providing options to the createRenderComponents function:

import { createRenderComponents, RichTextOptions } from "storyblok-nextjs";
import { RenderOptions } from "storyblok-rich-text-react-renderer";

const richTextOptions: RichTextOptions = {
  // Customize the render options
  customize: (defaultOptions: RenderOptions) => ({
    ...defaultOptions,
    // Add custom resolvers or modify existing ones
    markResolvers: {
      ...defaultOptions.markResolvers,
      bold: (children) => <strong className="font-bold text-primary">{children}</strong>,
    }
  }),
  
  // Control document transformations
  hoistImages: true, // Default: true - Lifts images out of paragraphs 
  inlineComponents: /inline/i, // Default: /inline/i - Regex pattern to identify inline components
  
  // Add custom transformation function
  transform: (node) => {
    // Apply additional custom transformations to the rich text node
    return node;
  }
};

export const Render = createRenderComponents(blocks, richTextOptions);

Available Rich Text Options

  • customize: A function that takes the default render options and returns customized options
  • hoistImages: (boolean, default: true) - When true, images are lifted out of paragraphs
  • inlineComponents: (boolean | RegExp, default: /inline/i) - Identifies components that should be treated as inline elements
    • true: Uses the default pattern /inline/i
    • false: Disables inline component processing
    • RegExp: Custom pattern to match component names against
  • transform: (function) - A custom function that takes a RichText document and returns a transformed one

This allows you to customize how rich text is rendered throughout your application without having to modify the base components. See https://www.npmjs.com/package/storyblok-rich-text-react-renderer for details about the render options.

4. Create a StoryblokNext instance

Next, we'll create an instance of StoryblokNext. It takes a couple of options that we will cover later. For now, all you need is a Storyblok preview token. If omitted, StoryblokNext will try to read it from the STORYBLOK_PREVIEW_TOKEN env variable.

// /storyblok.ts

import { StoryblokNext } from "storyblok-nextjs/server";

export const sb = new StoryblokNext({
  previewToken: "YOUR_PREVIEW_TOKEN"
});

5. Create some routes

Using the Next.js App Router, create the following three routes:

app
├── [[...slug]]
│   └── page.tsx
├── preview
│   └── [...slug]
│       └── page.tsx
└── admin
    └── page.tsx
    └── webhook
        └── route.ts
  • /[[...slug]] – The default route to serve the server-rendered published pages
  • /preview/[...slug] – Route for live previews in the Visual Editor
  • /admin – The Storyblok admin UI served under our domain
  • /admin/webhook – A webhook to invalidate stories upon publish

This is the most basic setup. For multilingual websites, the default route would be /[lang]/[[...slug]] instead.

5.1 The default route

For the default route, we'll use the page() method of the StoryblokNext instance we created earlier. It will return a React Server component that loads a published story using the requested slug (and optionally lang) and renders it using the provided Render components:

// /app/[[...slug]]/page.tsx

import { sb } from "@/storyblok";
import { Render } from "@/blocks";

export const dynamic = "error";
export const dynamicParams = true;

export default sb.page(Render);

5.2 The preview route

The preview route works quite similar, but instead of the Render components, we need to pass a Client Component to the previewPage() method:

// /app/preview/[...slug]/page.tsx

import { sb } from "@/storyblok";
import Preview from "./Preview";

export default sb.previewPage(Preview);

This is what the Preview component looks like:

// /app/preview/[...slug]/Preview.tsx

"use client";
import { Render } from "@/blocks";
export default Render.LivePreview;

The important part is the "use client" directive at the top of the file. Everything from down here will happen in the client. This is what allows us to get the instant live previews inside Storyblok's visual editor.

5.3 The admin route

The admin route is optional, you can also use https://app.storyblok.com directly. The advantage of this approach is that you don't need to set up HTTPS for local development. It also allows you to add you own branding to the login page via CSS.

// /app/admin/page.tsx

import { sb } from "@/storyblok";
export default sb.adminPage();

5.3 The webhook

This route provides a webhook that Storyblok can call if a story gets published or deleted. This will invalidate the data in the Next.js cache that has been tagged with the story.

// /app/admin/webhook/route.ts

import { sb } from "@/storyblok";
export const POST = sb.webhook({
  validate: true,
  secret: "MY-WEBHOOK-SECRET"
});

Additional data fetching

Some of your blocks might need to fetch additional data, based on their own properties. This can be done with dataResolvers. A resolver receives the block data from the story and a context and can use both to asynchronously fetch additional data.

Let's say we want to add a navigation menu to our pages that lists all stories in the root folder. First, we extend our page block and add a menu property:

// /blocks/Page.tsx

import { Block } from "storyblok-nextjs";
import { Render } from "@/blocks";

type Props = {
  body: Block[];
  menu: Array<{
    title: string;
    link: string;
  }>
};

export default function Page({ body, menu }: Props) {
  return (
    <div>
      <div>
        {menu.map((item, i) => (
          <a key={i} href={link}>{title}</a>
        )}
      </div>
      <Render.List blocks={body} />
    </div>
  );
}

With the new property in place, we can now configure a data resolver to populate it:

// /storyblok.ts

import { StoryblokNext } from "storyblok-nextjs";
import { blocks } from "@/blocks";

export const sb = new StoryblokNext<typeof blocks>({
  dataResolvers: {
    async page(props, { loader }) {
      const rootStories = await loader.getStories({
        level: 1,
      });
      return {
        menu: rootStories.map((s) => ({
          title: s.name,
          link: s.public_url!,
        })),
      };
    },
  },
});

Image Optimization

Use StoryblokImage, a wrapper around next/image to render Storyblok assets using a custom loader:

import { StoryblokImage } from "storyblok-nextjs";

export function Hero({ image }) {
  return <StoryblokImage {...image} />;
}