Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,9 +524,7 @@ sbBridge.on(['input', 'published', 'change'], (event) => {

## Rendering Rich Text

> [!WARNING]
> We have identified issues with richtext and Types on React 19 and Next.js 15. As a temporary measure, we advise you to continue using React 18 and Next.js 14 until we have fully resolved the issues.

### Client-side Rich Text Rendering
You can render rich text fields by using the `StoryblokRichText` component:

```ts
Expand Down Expand Up @@ -569,7 +567,61 @@ function App() {
}
```

For a comprehensive list of options you can provide to the `useStoryblokRichText`, please consult the [Full options](https://github.com/storyblok/richtext?tab=readme-ov-file#options) documentation.
### Server-side Rich Text Rendering

> [!INFO]
> Recommended for Next.js 15 and React 19.

You can render rich text fields by using the `StoryblokServerRichText` component:

```ts
import { StoryblokServerRichText, StoryblokStory } from '@storyblok/react/rsc';
import { StoryblokClient, ISbStoriesParams } from '@storyblok/react';
import { getStoryblokApi } from '@/lib/storyblok'; // Remember to import from the local file

async function App() {
const { data } = await fetchData();

if (!data?.story?.content) {
return <div>Loading...</div>;
}

return (
<div>
<StoryblokServerRichText doc={data.story.content.richText} />
</div>
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this example we are using the rsc module which is meant for Next.js App Router, but the code does not reflect the changes in the DX (they are documented in the README).

So either we change the example detailing that this is for App Router and reuse the same code, or we document that this is a non-strict example that only details how to use the StoryblokServerRichText component but everything else needs to be adapted to the user's context.


export async function fetchData() {
let sbParams: ISbStoriesParams = { version: 'draft' };

const storyblokApi: StoryblokClient = getStoryblokApi();
return storyblokApi.get(`cdn/stories/home`, sbParams);
}
```

Or you can have more control by using the `useStoryblokServerRichText` hook:

```ts
import { useStoryblokServerRichText, convertAttributesInElement } from '@storyblok/react';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are them not exported from the rsc module?

import Codeblock from './Codeblock';

function App() {
const { render } = useStoryblokServerRichText({
// options like resolvers
});

const html = render(doc);
const formattedHtml = convertAttributesInElement(html as React.ReactElement); // JSX

return (
<div ref={ref}>
{formattedHtml}
</div>
);
}
```

### Overriding the default resolvers

Expand Down
2 changes: 2 additions & 0 deletions playground/next15/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

certificates
8 changes: 6 additions & 2 deletions playground/next15/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev --experimental-https",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@storyblok/react": "workspace:^",
"@tailwindcss/postcss": "^4.0.14",
"@tailwindcss/typography": "^0.5.16",
"next": "15.3.2",
"postcss": "^8.5.3",
"react": "19.1.0",
"react-dom": "19.1.0"
"react-dom": "19.1.0",
"tailwindcss": "^4.0.14"
},
"devDependencies": {
"@types/node": "^22",
Expand Down
8 changes: 8 additions & 0 deletions playground/next15/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */

const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
18 changes: 14 additions & 4 deletions playground/next15/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';

span[data-type='emoji'] img {
@apply m-0;
}

:root {
--background: #ffffff;
--foreground: #171717;
Expand All @@ -24,10 +31,13 @@ body {
-moz-osx-font-smoothing: grayscale;
}

* {
box-sizing: border-box;
padding: 0;
margin: 0;
@layer base {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
scroll-behavior: smooth;
}
}

a {
Expand Down
1 change: 1 addition & 0 deletions playground/next15/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import StoryblokProvider from '@/components/StoryblokProvider';
import '../app/globals.css';

export const metadata = {
title: 'Create Next App',
Expand Down
123 changes: 26 additions & 97 deletions playground/next15/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,111 +1,40 @@
import type {
ISbStoriesParams,
StoryblokClient,
StoryblokRichTextNode,
} from '@storyblok/react/rsc';
import { MarkTypes, StoryblokRichText, StoryblokStory,
} from '@storyblok/react/rsc';
import { getStoryblokApi } from '@/lib/storyblok';
import type { ISbStoriesParams, StoryblokClient } from '@storyblok/react/rsc';
import { StoryblokStory } from '@storyblok/react/rsc';
import { getStoryblokApi } from '@/lib/storyblok'; // Remember to import from the local file
import Link from 'next/link';
import type { ReactElement } from 'react';

export default async function Home() {
const { data } = await fetchData();

const doc = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This is a test of the StoryblokRichText component.',
},
],
},
{
type: 'paragraph',
content: [
{
text: 'Internal Link',
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: '/',
uuid: '8489bed8-d86f-4fde-965c-e3d748e12147',
anchor: null,
target: '_self',
linktype: 'story',
},
},
],
},
],
},
{
type: 'paragraph',
content: [
{
text: 'External link',
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://alvarosaburido.dev',
uuid: null,
anchor: null,
target: '_blank',
linktype: 'url',
},
},
],
},
],
},
],
};
const resolvers = {
// custom resolvers
[MarkTypes.LINK]: (node: StoryblokRichTextNode<ReactElement>) => {
return node.attrs?.linktype === 'story'
? (
<Link
href={node.attrs?.href}
target={node.attrs?.target}
>
{node.text}
</Link>
)
: (
<a
href={node.attrs?.href}
target={node.attrs?.target}
>
{node.text}
</a>
);
},
};

return (
<div>
<h1>
Story:
{data.story.id}
</h1>
<StoryblokStory story={data.story} />
<StoryblokRichText doc={doc} resolvers={resolvers} />
</div>
<main className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl font-bold mb-8 dark:text-white">
Storyblok Next.js 15 Example
</h1>

<nav className="space-y-4">
<Link
href="/richtext"
className="block p-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
Go to Rich Text Example
</Link>
</nav>

{data.story && (
<div>
<StoryblokStory story={data.story} />
</div>
)}
</div>
</main>
);
}

export async function fetchData() {
const sbParams: ISbStoriesParams = { version: 'draft' };

const storyblokApi: StoryblokClient = getStoryblokApi();
return storyblokApi.get(`cdn/stories/home`, sbParams);
return storyblokApi.get(`cdn/stories/react`, sbParams);
}
37 changes: 37 additions & 0 deletions playground/next15/src/app/richtext/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ISbStoriesParams, StoryblokClient } from '@storyblok/react/rsc';
import { getStoryblokApi } from '@/lib/storyblok';
import { StoryblokServerRichText } from '@storyblok/react/rsc';

export default async function RichtextPage() {
const { data } = await fetchData();

if (!data.story?.content) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-pulse text-lg text-gray-600 dark:text-gray-400">
Loading content...
</div>
</div>
);
}

return (
<div className="container mx-auto px-4 py-8 prose prose-lg dark:prose-invert max-w-4xl">
<h1 className="text-3xl font-bold mb-8">Rich Text Example</h1>
{data.story.content.richText
? (
<StoryblokServerRichText doc={data.story.content.richText} />
)
: (
<p className="text-gray-600 dark:text-gray-400">No content available</p>
)}
</div>
);
}

export async function fetchData() {
const sbParams: ISbStoriesParams = { version: 'draft' };

const storyblokApi: StoryblokClient = getStoryblokApi();
return storyblokApi.get(`cdn/stories/react/test-richtext`, sbParams);
}
51 changes: 51 additions & 0 deletions playground/next15/src/components/EmojiRandomizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';

import React, { type FC, useState } from 'react';
import type { SbBlokData } from '@storyblok/react';

interface EmojiRandomizerProps {
blok: SbBlokData & {
label?: string;
};
}

/**
* A component that displays a label and a random emoji that changes on click
*/
const EmojiRandomizer: FC<EmojiRandomizerProps> = ({ blok }) => {
// List of fun emojis to randomly choose from
const emojis = ['😊', '🎉', '🚀', '✨', '🌈', '🎨', '🎸', '🎮', '🍕', '🌺'];

// State to track current emoji
const [currentEmoji, setCurrentEmoji] = useState(() =>
emojis[Math.floor(Math.random() * emojis.length)],
);

/**
* Generates a new random emoji different from the current one
*/
const randomizeEmoji = () => {
let newEmoji;
do {
newEmoji = emojis[Math.floor(Math.random() * emojis.length)];
} while (newEmoji === currentEmoji);

setCurrentEmoji(newEmoji);
};

return (
<div className="flex flex-col items-center gap-6 p-4 bg-gray-100 dark:bg-gray-900 rounded-lg">
<div className="text-6xl">
{currentEmoji}
</div>
<button
onClick={randomizeEmoji}
className="px-6 py-3 rounded-lg bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white font-small transition-colors duration-200 dark:bg-blue-600 dark:hover:bg-blue-700 dark:active:bg-blue-800"
>
{blok.label || 'Randomize Emoji'}
</button>
</div>
);
};

export default EmojiRandomizer;
2 changes: 2 additions & 0 deletions playground/next15/src/lib/storyblok.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import EmojiRandomizer from '@/components/EmojiRandomizer';
import Grid from '@/components/Grid';
import IFrameEmbed from '@/components/IFrameEmbed';
import Page from '@/components/Page';
Expand All @@ -12,5 +13,6 @@ export const getStoryblokApi = storyblokInit({
'page': Page,
'grid': Grid,
'iframe-embed': IFrameEmbed,
'emoji-randomizer': EmojiRandomizer,
},
});
Loading
Loading