diff --git a/playground/react/App.tsx b/playground/react/App.tsx
index 26d8fb27..5f189be7 100644
--- a/playground/react/App.tsx
+++ b/playground/react/App.tsx
@@ -1,14 +1,31 @@
-import { StoryblokComponent, useStoryblok } from '@storyblok/react';
import React from 'react';
+import { BrowserRouter, Link, Route, Routes } from 'react-router';
+import Home from './pages/Home';
+import RichtextPage from './pages/RichtextPage';
function App() {
- const story = useStoryblok('home', { version: 'draft' });
+ return (
+
+
+
- if (!story?.content) {
- return
Loading...
;
- }
-
- return
;
+
+ } />
+ } />
+ } />
+
+
+
+ );
}
export default App;
diff --git a/playground/react/index.tsx b/playground/react/index.tsx
index 92d30608..608ede26 100644
--- a/playground/react/index.tsx
+++ b/playground/react/index.tsx
@@ -11,7 +11,7 @@ import { apiPlugin, storyblokInit } from '@storyblok/react';
import IFrameEmbed from './components/iframe-embed';
storyblokInit({
- accessToken: 'd6IKUtAUDiKyAhpJtrLFcwtt',
+ accessToken: 'OurklwV5XsDJTIE1NJaD2wtt',
use: [apiPlugin],
components: {
'teaser': Teaser,
diff --git a/playground/react/package.json b/playground/react/package.json
index 3c8b18d3..547bf698 100644
--- a/playground/react/package.json
+++ b/playground/react/package.json
@@ -9,11 +9,13 @@
"dependencies": {
"@storyblok/react": "workspace:^",
"react": "^18.3.1",
- "react-dom": "^18.3.1"
+ "react-dom": "^18.3.1",
+ "react-router": "^7.1.1"
},
"devDependencies": {
"@types/node": "^20.17.10",
"@types/react": "18.3.4",
+ "@vitejs/plugin-basic-ssl": "^1.2.0",
"vite": "^5.4.3"
}
}
diff --git a/playground/react/pages/Home.tsx b/playground/react/pages/Home.tsx
new file mode 100644
index 00000000..374dae01
--- /dev/null
+++ b/playground/react/pages/Home.tsx
@@ -0,0 +1,19 @@
+import { StoryblokComponent, useStoryblok } from '@storyblok/react';
+import React from 'react';
+
+function Home() {
+ const story = useStoryblok('react', { version: 'draft' });
+
+ if (!story?.content) {
+ return
Loading...
;
+ }
+
+ return (
+
+
Home
+
+
+ );
+}
+
+export default Home;
diff --git a/playground/react/pages/RichtextPage.tsx b/playground/react/pages/RichtextPage.tsx
new file mode 100644
index 00000000..f8b68c16
--- /dev/null
+++ b/playground/react/pages/RichtextPage.tsx
@@ -0,0 +1,16 @@
+import { StoryblokRichText, useStoryblok } from '@storyblok/react';
+import React from 'react';
+
+function RichtextPage() {
+ const story = useStoryblok('react/test-richtext', { version: 'draft' });
+
+ if (!story?.content) {
+ return Loading...
;
+ }
+
+ return (
+ story.content.richText &&
+ );
+}
+
+export default RichtextPage;
diff --git a/playground/react/vite.config.ts b/playground/react/vite.config.ts
index 0dde4f9d..23d87078 100644
--- a/playground/react/vite.config.ts
+++ b/playground/react/vite.config.ts
@@ -1,9 +1,13 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'node:path';
+import basicSsl from '@vitejs/plugin-basic-ssl';
export default defineConfig({
- plugins: [react()],
+ plugins: [
+ react(),
+ basicSsl(),
+ ],
resolve: {
alias: {
'@storyblok/react': resolve(__dirname, '../../src/index.ts'),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 86bdf9b9..8b75407e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -180,6 +180,9 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
+ react-router:
+ specifier: ^7.1.1
+ version: 7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@types/node':
specifier: ^20.17.10
@@ -187,6 +190,9 @@ importers:
'@types/react':
specifier: 18.3.4
version: 18.3.4
+ '@vitejs/plugin-basic-ssl':
+ specifier: ^1.2.0
+ version: 1.2.0(vite@5.4.11(@types/node@20.17.10))
vite:
specifier: ^5.4.3
version: 5.4.11(@types/node@20.17.10)
@@ -1905,6 +1911,9 @@ packages:
'@types/conventional-commits-parser@5.0.0':
resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==}
+ '@types/cookie@0.6.0':
+ resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -2045,6 +2054,12 @@ packages:
'@ungap/structured-clone@1.2.1':
resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==}
+ '@vitejs/plugin-basic-ssl@1.2.0':
+ resolution: {integrity: sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==}
+ engines: {node: '>=14.21.3'}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
+
'@vitejs/plugin-react@4.3.4':
resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -2572,6 +2587,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie@1.0.2:
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+ engines: {node: '>=18'}
+
core-js-compat@3.39.0:
resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==}
@@ -4611,6 +4630,16 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
+ react-router@7.1.1:
+ resolution: {integrity: sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -4787,6 +4816,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -5116,6 +5148,9 @@ packages:
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
+ turbo-stream@2.4.0:
+ resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
+
tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
@@ -7359,6 +7394,8 @@ snapshots:
dependencies:
'@types/node': 20.17.12
+ '@types/cookie@0.6.0': {}
+
'@types/debug@4.1.12':
dependencies:
'@types/ms': 0.7.34
@@ -7595,6 +7632,10 @@ snapshots:
'@ungap/structured-clone@1.2.1': {}
+ '@vitejs/plugin-basic-ssl@1.2.0(vite@5.4.11(@types/node@20.17.10))':
+ dependencies:
+ vite: 5.4.11(@types/node@20.17.10)
+
'@vitejs/plugin-react@4.3.4(vite@6.0.6(@types/node@20.17.12)(jiti@2.4.2)(yaml@2.6.0))':
dependencies:
'@babel/core': 7.26.0
@@ -8195,6 +8236,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie@1.0.2: {}
+
core-js-compat@3.39.0:
dependencies:
browserslist: 4.24.2
@@ -8685,7 +8728,7 @@ snapshots:
debug: 4.4.0(supports-color@8.1.1)
enhanced-resolve: 5.17.1
eslint: 8.57.0
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import-x@4.4.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
@@ -8709,7 +8752,7 @@ snapshots:
dependencies:
eslint: 9.17.0(jiti@2.4.2)
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import-x@4.4.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7(supports-color@8.1.1)
optionalDependencies:
@@ -8801,7 +8844,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import-x@4.4.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -10938,6 +10981,16 @@ snapshots:
react-refresh@0.14.2: {}
+ react-router@7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@types/cookie': 0.6.0
+ cookie: 1.0.2
+ react: 18.3.1
+ set-cookie-parser: 2.7.1
+ turbo-stream: 2.4.0
+ optionalDependencies:
+ react-dom: 18.3.1(react@18.3.1)
+
react@18.3.1:
dependencies:
loose-envify: 1.4.0
@@ -11140,6 +11193,8 @@ snapshots:
semver@7.6.3: {}
+ set-cookie-parser@2.7.1: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -11504,6 +11559,8 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
+ turbo-stream@2.4.0: {}
+
tweetnacl@0.14.5: {}
type-check@0.4.0:
diff --git a/src/utils.ts b/src/utils.ts
index 2f1acc82..8facc4f6 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,5 +1,11 @@
import React from 'react';
+function decodeHtmlEntities(text: string): string {
+ const textarea = document.createElement('textarea');
+ textarea.innerHTML = text;
+ return textarea.value;
+}
+
function camelCase(str: string) {
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
}
@@ -29,6 +35,10 @@ export function convertAttributesInElement(element: React.ReactElement | React.R
// Base case: if the element is not a React element, return it unchanged.
if (!React.isValidElement(element)) {
+ // If it's a text node, decode any HTML entities
+ if (typeof element === 'string') {
+ return decodeHtmlEntities(element) as unknown as React.ReactElement;
+ }
return element;
}
@@ -55,7 +65,13 @@ export function convertAttributesInElement(element: React.ReactElement | React.R
newProps.key = (element.key as string);
// Process children recursively.
- const children = React.Children.map((element.props as React.PropsWithChildren).children, child => convertAttributesInElement(child as React.ReactElement));
+ const children = React.Children.map((element.props as React.PropsWithChildren).children, (child) => {
+ if (typeof child === 'string') {
+ return decodeHtmlEntities(child);
+ }
+ return convertAttributesInElement(child as React.ReactElement);
+ });
+
const newElement = React.createElement(element.type, newProps, children);
// Clone the element with the new properties and updated children.
return newElement;