storyblok / storyblok-react

React SDK for Storyblok CMS
113 stars 36 forks source link

Correct setup for live editing React Next v14 (not using App router) #1115

Closed grantspilsbury closed 3 weeks ago

grantspilsbury commented 4 weeks ago

Describe the issue you're facing

I am trying to get live editing working for @storyblok-react on my Nextjs 14 site (not using App router). The HTML looks ok but live editing is not working. The latest version of https://app.storyblok.com/f/storyblok-v2-latest.js is being loaded.

  1. Am I missing anything in my Storyblok setup?
  2. How do I use StoryblokComponent if I have a component mapper?
  3. Do I need a StoryblokProvider?
  4. How do I use StoryblokComponent in my case (component mapper)?

This is the code I have added:

import {
  storyblokInit,
  apiPlugin,
  useStoryblok,
  getStoryblokApi,
  storyblokEditable
} from "@storyblok/react";

// Function to get story by slug
const getStoryBySlug = async (slug, preview, options) => {
  const params = {
    version: preview ? 'draft' : 'published',
    cv: Date.now(),
    ...options
  }

  const storyblokApi = getStoryblokApi()
  const { data } = await storyblokApi.get(`cdn/stories/${slug}`, params)
  return data.story
}

export const mapPanel = (data) => ({
  title: data.title.replace(/(^"|"$)/g, '')
})

// Sample Panel component
const Panel = ({ title }) => (
  <div className="panel">
    {title}
  </div>
)

const componentMap = {
  panel: [Panel, mapPanel]
}

// Components map
const components = {
  panel: Panel,
  // Add other components here
}

// Initialize Storyblok
storyblokInit({
  accessToken: 'key',
  use: [apiPlugin],
  components
})

// Custom component mapper
const ComponentMapper = ({ blok }) => {
  const [Component, mapFn] = componentMap[blok.component] ?? []
  const props = mapFn ? mapFn(blok) : {}
  return (
    <div {...storyblokEditable(blok)} key={blok._uid}>
      <Component {...props} />
    </div>
  )
}

// Home component
const Home = ({ story, preview }) => {
  const params = React.useMemo(
    () => ({
      version: preview ? 'draft' : 'published',
      cv: Date.now()
    }),
    [preview]
  )

  const editableContent = useStoryblok(story.slug, params)
  const content = editableContent?.content || {}
  const { panel = [], body = [] } = content

  return (
    <Layout preview={preview} slug={story.full_slug}>
      <main>
        {(panel ?? []).map((blok) => (
          <ComponentMapper blok={blok} key={blok._uid} />
        ))}
        {(body ?? []).map((blok) => (
          <ComponentMapper blok={blok} key={blok._uid} />
        ))}
      </main>
    </Layout>
  )
}

export default Home

// Static props for the Home component
export const getStaticProps = async (context) => {
  const story = await getStoryBySlug('/', context.preview)
  return {
    props: {
      story,
      preview: !!context.preview
    },
    revalidate: config.revalidateISRTime
  }
}

When logging editable content it also looks ok:

{
    "_uid": "63d3a889-ef10-4173-9547-5bcad8",
    "hero_panel": [
        {
            "_uid": "6507dca1-66ec-4996-8ed8-fc098",
            "style": "section",
            "heading": "Welcome.",
            "component": "panel",
            "_editable": "<!--#storyblok#{\"name\": \"panel\", \"space\": \"123\", \"uid\": \"6507dca1-66ec-4996-8ed8-fc098\", \"id\": \"11\"}-->"
        }
    ],
    "_editable": "<!--#storyblok#{\"name\": \"home\", \"space\": \"123\", \"uid\": \"63d3a889-ef10-4173-9547-5bcad\", \"id\": \"11\"}-->"
}

The rendered HTML looks something like this Screenshot 2024-06-11 at 11 26 34 AM

Reproduction

https://stackblitz.com/edit/stackblitz-starters-uh3eae?file=pages%2Findex.jsx

Steps to reproduce

No response

System Info

@storyblok-react 3.0.10
next 14.2.3

Used Package Manager

npm

Error logs (Optional)

No response

Validations

arorachakit commented 4 weeks ago

Hey @grantspilsbury ! I tried the stackblitz link with a couple of changes - I just removed the preview/config options for testing. And it works for me!

What I mean is the setup you have works. Regarding questions -

  1. I think the context is an issue here, because it is falsy - it might have been loading the published version which is not editable. But as you consoled it, it looks good - so not sure.
  2. If you have your own mapper, then you don't need StoryblokComponent.
  3. No you don't need the provider in this case

Also - just a suggestion, since you're already fetching the story in StaticProps, you don't need to fetch that again, you can just use useStoryblokState

Here is the code, that I tried (it is yours, I just removed a few things to test and added useStoryblokState as well, though it works with useStoryblok too) Please let me know if you face more issues, or if live editing is still an issue. You can use this to compare and see if there was anything that was causing the issue.

Also, another quick question - is it happening on dev or prod or both?


import {
  storyblokInit,
  apiPlugin,
  useStoryblok,
  getStoryblokApi,
  storyblokEditable,
  useStoryblokState
} from "@storyblok/react";
import React from 'react'

// Function to get story by slug
const getStoryBySlug = async (slug) => {
  const params = {
    version: 'draft',
    cv: Date.now()
  }

  const storyblokApi = getStoryblokApi()
  const { data } = await storyblokApi.get(`cdn/stories/${slug}`, params)
  return data.story
}

export const mapPanel = (data) => ({
  title: data.title.replace(/(^"|"$)/g, '')
})

// Sample Panel component
const Panel = ({ title }) => (
  <div className="panel">
    {title}
  </div>
)

const componentMap = {
  panel: [Panel, mapPanel]
}

// Components map
const components = {
  panel: Panel,
  // Add other components here
}

// Initialize Storyblok
storyblokInit({
  accessToken: 'Td6uuk4Q2JYJH3uzOuOyIgtt',
  use: [apiPlugin],
  components
})

// Custom component mapper
const ComponentMapper = ({ blok }) => {
  const [Component, mapFn] = componentMap[blok.component] ?? []
  const props = mapFn ? mapFn(blok) : {}
  return (
    <div {...storyblokEditable(blok)} key={blok._uid}>
      <Component {...props} />
    </div>
  )
}

// Home component
const Home = ({ story }) => {

  const editableContent = useStoryblokState(story)
  const content = editableContent?.content || {}
  const { panel = [], body = [] } = content

  return (

      <main>
        {(panel ?? []).map((blok) => (
          <ComponentMapper blok={blok} key={blok._uid} />
        ))}
        {(body ?? []).map((blok) => (
          <ComponentMapper blok={blok} key={blok._uid} />
        ))}
      </main>
  )
}

export default Home

// Static props for the Home component
export const getStaticProps = async (context) => {
  console.log(context)
  const story = await getStoryBySlug('home')
  return {
    props: {
      story
    },
    revalidate: 3600
  }
}```
grantspilsbury commented 3 weeks ago

arorachakit

Thanks for the help. Can you help me understand 2 things: 1) What could be the reasons that nextJs draftMode is working when I run my app outside Storyblok (http://localhost:3000/api/draft?secret=corp-secret&slug=pages/) but when I run it inside storyblok (https://localhost:3010/api/draft?secret=corp-secret&slug=pages/) and view it in the Storyblok preview window draftMode().isEnabled is always false. 2) What could the reasons be for the storyblok-v2-latest.js script not to load when I run storyblokInit? Previously I could see it in the networks tab but since moving to nextjs 14 with /app directory I can’t get it to load. I have tried initialising it in client and server components as well as the StoryblokProvider. I can print out config.storyblokApiKey and it is initialising with the correct key.

'use client'

import { storyblokInit, apiPlugin } from '@storyblok/react'
import config from 'config'

interface Props {
  children: React.ReactNode
}

console.log(config.storyblokApiKey)

storyblokInit({
  accessToken: config.storyblokApiKey,
  use: [apiPlugin],
  apiOptions: {
    cache: {
      clear: 'auto',
      type: 'memory'
    }
  },
  bridge: process.env.NODE_ENV !== 'production'
})
arorachakit commented 3 weeks ago

Hey @grantspilsbury ! I hope the previous issue was resolved.

  1. The reason of draft mode not working in the visual editor could be (most probably) related to the cookie and it's same site policy - you need to have same site as None for the cookie. As Storyblok is loading your content in an iframe, this could be the reason. Please take a look here, this is Next 12 example but should be similar for Next 14 - https://github.com/storyblok/next.js-ultimate-tutorial/blob/main/pages/api/preview.js#L20

  2. A couple of months ago we implemented a basic check to not load the bridge outside the visual editor, that could be the issue. Is it not even loading in the visual editor? For loading it outside the Visual Editor, simply append _storyblok_tk to the URL and it should work as expected.

grantspilsbury commented 2 weeks ago

I was using next: 14.2.3. I downgraded to next: 13.4.2 and it loads as expected

arorachakit commented 2 weeks ago

@grantspilsbury - do you mean keeping the same setup, it is an issue with different versions? I have a couple of more questions - Is it happening for the complete app, or just for any specific pages? Also, is it happening inside the Visual Editor or outside that?
Is the live editing also not working?

arorachakit commented 2 weeks ago

Also, another quick question - is this happening with just app directory?

arorachakit commented 2 weeks ago

Here is small project I did a couple of months ago with Next 14 and app directory - https://github.com/arorachakit/Restaurant-Guide-with-Next.js-and-Storyblok I can see the bridge load in the networks tab, please take a look - this might help.

Also let me know if there is still an issue, maybe you can share a small setup if that is something different to what you already shared. :)

grantspilsbury commented 2 weeks ago

It was working and then I started messing with next and @storyblok/react versions and now I can't get it to load the script.

On a side note are you able to share a tsconfig.json setup as I am getting typescript errors on: import { storyblokEditable } from '@storyblok/react/rsc'

Cannot find module '@storyblok/react/rsc' or its corresponding type declarations.
  There are types at '/website/node_modules/@storyblok/react/dist/types/rsc/index.d.ts', but this result could not be resolved under your current 'moduleResolution' setting. Consider updating to 'node16', 'nodenext', or 'bundler'.

My config file is:

{
  "compilerOptions": {
    "baseUrl": "./src",
    "lib": ["esnext", "dom", "dom.iterable"],
    "allowJs": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "outDir": "./dist",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    },
    "allowSyntheticDefaultImports": true,
    "target": "es6",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "skipLibCheck": true
  },
  "include": ["./src/**/*", "next-env.d.ts", ".next/types/**/*.ts"],
  "exclude": ["node_modules", "migrations"]
}
arorachakit commented 2 weeks ago

@grantspilsbury - Sure, also please take a look at the project I shared. It might be a good reference point.

For the tsconfig - the issue could be moduleResolution, please try changing it to nodenext or bundler instead of node. Let me know if it works with that :)

grantspilsbury commented 2 weeks ago

https://github.com/arorachakit/Restaurant-Guide-with-Next.js-and-Storyblok

Thanks for the help. Here's a sample repo: https://github.com/grantspilsbury/storyblok_nextjs_14_live_editing_preview The script is loading but the live editing is not working. Do you see any obvious issues?

I am mapping the incoming data from Storyblok and then displaying the component:

const ComponentMapper: React.FC<Props> = ({ blok }) => {
  const [Component, mapFn] = componentMap[blok.component]

  return (
    <div {...storyblokEditable(blok)} key={blok._uid}>
      <Component {...mapFn(blok)} />
    </div>
  )
}
export default ComponentMapper

// example of mapFn
export const mapTitle = (data) => ({
  title: data.title ?? '',
  description: data.description ?? '',
})

// example of Component
const Title: React.FC<Props> = ({ title, description }) => 
<div>
  <ComplicatedTitle title={title} />
  <div>{description}</div>
</div>
export default Title

I can see the data-block-c properties but it isn't live editing.

Screenshot 2024-06-20 at 3 11 06 PM

I see there is a StoryblokStory and StoryblokComponent which I'm not using. And because of that I'm not adding the components to storyblokInit. Is this incorrect?

arorachakit commented 2 weeks ago

Hey @grantspilsbury - the sample project isn't up to date I guess. In any case, I recommend you reading the readme of this repo https://github.com/storyblok/storyblok-react?tab=readme-ov-file#nextjs-using-app-router---live-editing-support along with the tutorial we have - https://www.storyblok.com/tp/add-a-headless-cms-to-next-js-13-in-5-minutes

It takes you through the two approaches - live editing and complete server side. Yes, StoryblokStory along with not using the StoryblokProvider can be the issue.

Please take a look at the resources to know more - and in case you want to have your own mapper, please feel free to check the code of StoryblokStory and StoryblokComponent in this repo itself.

grantspilsbury commented 2 weeks ago

Within storyblok's visual editor the v2 script loads when viewing a page but it doesnt load it when setting the draft / preview

It loads on: https://localhost:3010/pages/ but not on: https://localhost:3010/api/draft?secret=secret&slug=pages/

I get a 500 error (Failed to load resource: the server responded with a status of 500 (Internal Server Error)) which is logged in dev console but not in terminal. This is while using the proxy (https://localhost:3010 → http://localhost:3000) at https://localhost:3010/api/draft?secret=secret&slug=pages/

Do you think I have a Storyblok setup issue or it's not meant to load when access the draft route or maybe something to do with my draft route code:

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const slug = searchParams.get('slug') ?? '/'
  const story = await getPageBySlug(slug)

  if (!story) {
    return new Response('Invalid slug', { status: 401 })
  }

  draftMode().enable()

  cookies()
    .getAll()
    .forEach((cookie) => {
      cookies().set(cookie.name, cookie.value, {
        sameSite: 'none',
        secure: true
      })
    })

  return new Response(null, {
    status: 307,
    headers: {
      Location: `/${story.full_slug}`
    }
  })
}

Edit: on further inspection I also get the 500 error when viewing a page without the api/draft route https://localhost:3010/pages?_storyblok=1192xxx&_storyblok_c=home&_storyblok_version=&_storyblok_lang=default&_storyblok_release=0&_storyblok_rl=1718953xxx&_storyblok_tk%5Bspace_id%5D=69xxx&_storyblok_tk%5Btimestamp%5D=171895xxxx&_storyblok_tk%5Btoken%5D=b6f0520ea5a43c4d15xxx But the script does load

arorachakit commented 2 weeks ago

Hey @grantspilsbury ! There can be many reasons for the 500 error, not sure if I can help much here.

As the issue is already closed and we initially had a different topic - I recommend you taking a look at our discord. We also have a next.js channel over there, maybe you can find something over there. Also a couple of setups can be with the draft mode as well. That would be the correct place for such questions, the community is pretty awesome!

Please feel free to open any new issues if you find anything related to the SDK - and feel free to take a look at tutorials to see the setup and compare.

Thank you!

pvhuwung commented 1 week ago

Hi everyone, Im getting an error when doing live editing, also cannot toggle, for example if i change the text, or choose another banner, then the rest component become disappear, like this: image

Screenshot 2024-06-27 at 2 03 21 AM

Thanks everyone

pvhuwung commented 1 week ago

this only occurs when I changes some text, or live adjust the component, then it trigger to this, refresh will be revert to normal, but then the purpose of live editing become not able to perform

pvhuwung commented 1 week ago
Screenshot 2024-06-27 at 2 06 06 AM

Is this because of the API configure or the bridge, wonder what causing this issue. Thanks