Code Monkey home page Code Monkey logo

storyblok-demo's Introduction

@Jpkha/nextjs13-storyblok

Nextjs with App directory integration for the Storyblok Headless CMS.


Installation

Install @storyblok/js:

npm install @storyblok/js

Create a new file and add code to utils/storyblok.ts and replace the accessToken with the preview API token of your Storyblok space.

import { storyblokInit, apiPlugin } from '@storyblok/js'

const { storyblokApi } = storyblokInit({
  accessToken: '<your-access-token>',
  bridge: true,
  apiOptions: {
    cache: { type: 'memory' },
  },
  use: [apiPlugin],
})

export async function getLinks() {
  if (!storyblokApi) {
    return
  }
  const { data } = await storyblokApi.get('cdn/links', {
    version: 'draft',
  })
  const links = data ? data.links : null
  return links
}

export async function getStory(slug: string) {
  if (!storyblokApi) {
    return
  }
  const { data } = await storyblokApi.get(`cdn/stories/${slug}`, {
    version: 'draft',
  })
  const story = data ? data.story : null
  return story
}

Options

When you initialize the integration, you can pass all @storyblok/js options. For spaces created in the United States, you have to set the region parameter accordingly { apiOptions: { region: 'us' } }.

// Defaults
storyblok({
  accessToken: '<your-access-token>',
  bridge: true,
  apiOptions: {}, // storyblok-js-client options
  useCustomApi: false,
})

Note: By default, the apiPlugin from @storyblok/js is loaded. If you want to use your own method to fetch data from Storyblok, you can disable this behavior by setting useCustomApi to true, resulting in an optimized final bundle.

Getting started

1. Creating and linking your components to the Storyblok Visual Editor

In order to link your components to their equivalents you created in Storyblok:

First, let's create a StoryblokComponent where we can load dynamically the component StoryblokComponent.tsx:

import React, { FunctionComponent } from 'react'
import { ComponentsMap } from './components-list'
import { SbBlokData } from '@storyblok/js'

interface StoryblokComponentProps {
  blok: SbBlokData
    [key: string]: unknown
}

export const StoryblokComponent: FunctionComponent<StoryblokComponentProps> = ({ blok, ...restProps }) => {
  if (!blok) {
    console.error("Please provide a 'blok' property to the StoryblokComponent")
    return <div>Please provide a blok property to the StoryblokComponent</div>
  }
  if(blok.component) {
    const Component = getComponent(blok.component)
    if (Component) {
      return <Component blok={blok} {...restProps} />
    }
  }
  return <></>
}

const getComponent = (componentKey: string) => {
  // @ts-ignore
  if (!ComponentsMap[componentKey]) {
    console.error(`Component ${componentKey} doesn't exist.`)
    return false
  }
// @ts-ignore
  return ComponentsMap[componentKey]
}

Then create a file components-list.ts:

import PageStory from './page/PageStory'
import Grid from './grid/Grid'
import Feature from './feature/Feature'
import Teaser from './teaser/Teaser'
import { FunctionComponent } from 'react'
import { BlokComponentModel } from '../models/blok-component.model'

interface ComponentsMapProps {
  [key: string]: FunctionComponent<BlokComponentModel<any>>;
}
export const ComponentsMap: ComponentsMapProps = {
  page: PageStory,
  grid: Grid,
  feature: Feature,
  teaser: Teaser,
}

For each component, use the storyblokEditable() function on its root element, passing the blok property that they receive:

For the components Teaser for exemple create a file Teaser.tsx in the components folder:

import { FunctionComponent } from 'react'
import { BlokComponentModel } from '../../models/blok-component.model'
import { SbBlokData, storyblokEditable } from '@storyblok/js'

interface TeaserProps extends SbBlokData {
  headline: string;
}

const Teaser: FunctionComponent<BlokComponentModel<TeaserProps>> = ({
  blok,
}) => {
  return <h2 {...storyblokEditable(blok)}>{blok.headline}</h2>
}

export default Teaser

Finally, you can use the provided <StoryblokComponent> for nested components :

import { StoryblokComponent } from '../StoryblokComponent'
import { SbBlokData, storyblokEditable } from '@storyblok/js'
import { FunctionComponent } from 'react'
import { BlokComponentModel } from '../../models/blok-component.model'

interface PageStoryProps extends SbBlokData {
  body: SbBlokData[];
}
const PageStory: FunctionComponent<BlokComponentModel<PageStoryProps>> = ({
  blok,
}) => {
  return (
    <main {...storyblokEditable(blok)}>
      {blok.body.map((nestedBlok) => (
        <StoryblokComponent blok={nestedBlok} key={nestedBlok._uid} />
      ))}
    </main>
  )
}

export default PageStory

Note: The blok is the actual blok data coming from Storyblok's Content Delivery API.

Let's get our first Story page

In app/page.tsx :

import styles from './page.module.css'
import { getStory } from '../utils/storyblok'
import { previewData } from "next/headers";
import StoryblokBridge from "../components/StoryblokBridge";
import { StoryblokComponent } from "../components/StoryblokComponent";

async function fetchData() {
  const story = await getStory( 'home' )
  return {
    props: {
      story: story ?? false,
    },
  }
}

export default async function Home() {
  const { props } = await fetchData();
  const data = previewData() as {key: string};
  //Add this code if you need to use preview mode
  const isPreviewMode = !!data && data.key === 'MY_SECRET_TOKEN';
  const version = process.env.NEXT_PUBLIC_STORYBLOK_VERSION;
  return (
    <main className={ styles.container }>
      { isPreviewMode || version === 'draft' ?
        <StoryblokBridge blok={ props.story.content }/> :
        <StoryblokComponent blok={ props.story.content }/>
      }
    </main>
  )
}

2. Use the Storyblok Bridge

Use the loadStoryblokBridge function to have access to an instance of storyblok-js:

'use client'
import { loadStoryblokBridge, SbBlokData } from '@storyblok/js'
import { StoryblokComponent } from './StoryblokComponent'
import { useState } from 'react'

const StoryblokBridge = ({ blok }: { blok: SbBlokData }) => {
  const [blokState, setBlokState] = useState(blok)
  loadStoryblokBridge()
    .then(() => {
      const { StoryblokBridge, location } = window
      const storyblokInstance = new StoryblokBridge()
      storyblokInstance.on(['published', 'change'], () => {
        location.reload()
      })
      storyblokInstance.on(['input'], (e) => {
        setBlokState(e?.story?.content)
      })
    })
    .catch((err) => console.error(err))
  return <StoryblokComponent blok={blokState} />
}

export default StoryblokBridge

Dynamic Routing

In order to dynamically generate Nextjs pages based on the Stories in your Storyblok Space, you can use the Storyblok Links API and the Nextjs generateStaticParams() function similar to this example:

import styles from '../page.module.css'
import { StoryblokComponent } from '../../components/StoryblokComponent'
import { getLinks, getStory } from '../../utils/storyblok'
import StoryblokBridge from "../../components/StoryblokBridge";
import { previewData } from "next/headers";

interface Paths {
  slug: string[]
}
export async function generateStaticParams() {
  const links = await getLinks()
  const paths: Paths[] = [];
  Object.keys(links).forEach((linkKey) => {
    if (links[linkKey].is_folder || links[linkKey].slug === 'home') {
      return
    }

    const slug = links[linkKey].slug
    let splittedSlug = slug.split('/')
    paths.push({ slug: splittedSlug })
  })

  return paths
}

async function fetchData(params: Paths) {
  let slug = params.slug ? params.slug.join('/') : 'home'

  const story = await getStory(slug)
  return {
    props: {
      story: story ?? false,
    },
  }
}

export default async function Page({ params } : {params: Paths}) {
  const { props } = await fetchData(params);
  const data = previewData() as {key: string};
  //Add this code if you need to use preview mode
  const isPreviewMode = !!data && data.key === 'MY_SECRET_TOKEN';
  const version = process.env.NEXT_PUBLIC_STORYBLOK_VERSION;
  return (
    <main className={styles.container}>
      { isPreviewMode || version === 'draft' ?
        <StoryblokBridge blok={ props.story.content }/> :
        <StoryblokComponent blok={ props.story.content }/>
      }
    </main>
  )
}

Using the Storyblok Bridge and preview mode

Create an API route for the preview, it should be defined at pages/api/preview.js

export default async function preview(req, res) {
  const { slug = '' } = req.query
  // get the storyblok params for the bridge to work
  const params = req.url.split('?')

  // Check the secret and next parameters
  // This secret should only be known to this API route and the CMS
  if (req.query.secret !== 'MY_SECRET_TOKEN') {
    return res.status(401).json({ message: 'Invalid token' })
  }

  // Enable Preview Mode by setting the cookies
  const location = `/${slug}?${params[1]}`
  res.setPreviewData({
    key: req.query.secret,
  })

  // Set cookie to None, so it can be read in the Storyblok iframe
  const cookies = res.getHeader('Set-Cookie')
  res.setHeader(
    'Set-Cookie',
    cookies.map((cookie) =>
      cookie.replace('SameSite=Lax', 'SameSite=None;Secure')
    )
  )

  res.writeHead(307, { Location: location })
  res.end()
}

And in order to exit preview mode create a file pages/api/exit-preview.js

export default async function exit(req, res) {
  const { slug = '' } = req.query
  // Exit the current user from "Preview Mode". This function accepts no args.
  res.clearPreviewData({})

  // set the cookies to None
  const cookies = res.getHeader('Set-Cookie')
  res.setHeader(
    'Set-Cookie',
    cookies.map((cookie) =>
      cookie.replace('SameSite=Lax', 'SameSite=None;Secure')
    )
  )

  // Redirect the user back to the index page.
  res.redirect(`/${slug}`)
}

Support

storyblok-demo's People

Contributors

hadrysm avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.