O’Reilly Report: Decoupled Applications and Composable Web Architectures - Download Now

Empower your teams & get a 582% ROI: See Storyblok's CMS in action

Skip to main content

Create a Preview Environment for Your Next.js Website

Try Storyblok

Storyblok is the first headless CMS that works for developers & marketers alike.

  • Home
  • Tutorials
  • Create a Preview Environment for Your Next.js Website

In this tutorial, we will deploy our website on Vercel as a static site, taking full advantage of the Jamstack approach and Next.js. We will also see how to generate a preview for our app using the Preview Mode that Next.js provides, which allows rendering the pages in request time instead of build time, bypassing the static site generation. This will enable live editing on our deployed environment. We will do a separate deployment for our production environment to make it more optimized. 

HINT:

You can read more about the Next.js Preview Mode here.

We will be taking advantage of this Preview Mode to load the different versions of the content - draft when previewing, and published otherwise. 

HINT:

If you're using Next.js 13 or newer, you should check draft mode here.

HINT:

If you’re in a hurry, you can explore or fork the code from the Next Ultimate Tutorial GitHub Repository.

Requirements

This is a part of the Ultimate Tutorial Guide for Next.js. You can find the previous part of the series here, which shows how to add and manage multiple languages in Storyblok and Next.js. We recommend you to have completed that tutorial before starting this one.

HINT:

We will be using the code from the previous tutorial as a starting point. You can find it here.

Adding Preview Mode

To enable Next.js Preview Mode we will need to create two files inside a new folder named api: pages/api/preview.js and pages/api/exit-preview.js.

HINT:

Any file inside the folder pages/api is mapped to /api/* and will be treated as an API endpoint instead of a page. Read more about it here.

If you don't already have this folder, create one inside the pages folder.

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
  res.setPreviewData({});
 
  // 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")
    )
  );
 
  // Redirect to the path from entry
  res.redirect(`/${slug}?${params[1]}`);
}
    

The preview.js file will check for a secret token, that needs to be set on the server-side. If the token matches, it will set the Next.js preview cookie. Since the Storyblok Visual Editor loads your site inside an iframe, we need to change the SameSite policy of this cookie to None, so the preview cookie can also be read inside the iframe.

To exit the preview mode, we will need a api/exit-preview.js file with the following code:

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}`);
}
    

When the preview mode is enabled, the function getStaticProps will be called at request time instead of build time. 

Adding a preview URL

These two files will have two paths associated with them, one for enabling preview mode and the other for exiting it. Now we need to add these URLs to our Visual Editor. Inside the Visual Editor, we can click on Change URL {1} and on Add or change preview URLs {2}.

Add or change preview URLs
1
2

Add or change preview URLs

This will take us to the Visual Editor settings. Here, we will add two new preview URLs: Preview and Exit Preview. The preview URL will be https://localhost:3010/api/preview?secret=MY_SECRET_TOKEN&slug= .

The secret token needs to be the same as the token you set in your preview function in Next.js. This token is independent of Storyblok.

Once you open this route with the correct secret token, the Next.js preview cookie will be set. Storyblok will automatically append the slug for the current story for you. To exit the Preview Mode, just add another URL - https://localhost:3010/api/exit-preview?slug=. Once you open this exit preview path, the cookie for the preview will be reset.

HINT:

Once we deploy our website, we will change the url to the deployed one instead of localhost.

Having added the preview mode, let's use it to configure content versions to work accordingly.

Loading the Draft Version only in Preview Mode

Once we add the preview mode, we can use context.preview which is available inside getStaticProps. Using this we can fetch different versions of data: draft for the preview URL and published otherwise.

Replace the code inside [...slug].js with the following:

[...slug].js
        
      import Head from "next/head";
import Layout from "../components/Layout";
import {
  useStoryblokState,
  getStoryblokApi,
  StoryblokComponent,
} from "@storyblok/react";
export default function Page({ story, locales, locale, defaultLocale, preview }) {
  story = useStoryblokState(story, {
    language: locale
  });
  return (
    <div >
      <Head>
        <title>{story ? story.name : "My Site"}</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Layout locales={locales} locale={locale} defaultLocale={defaultLocale}>
        <StoryblokComponent blok={story.content} locale={locale}  />
      </Layout>
    </div>
  );
}
export async function getStaticProps({ params, locales, locale, defaultLocale, preview }) {
  let slug = params.slug ? params.slug.join("/") : "home";
  let sbParams = {
    version: preview? "draft" : "published",
    language: locale
  };
  const storyblokApi = getStoryblokApi();
  let { data } = await storyblokApi.get(`cdn/stories/${slug}`, sbParams);
  return {
    props: {
      locales, 
      locale, 
      defaultLocale,
      story: data ? data.story : false,
      key: data ? data.story.id : false,
      preview: preview || false
    },
    revalidate: 3600,
  };
}
export async function getStaticPaths({locales}) {
  const storyblokApi = getStoryblokApi();
  let { data } = await storyblokApi.get("cdn/links/" ,{
    version: 'published'
  });
  let paths = [];
  Object.keys(data.links).forEach((linkKey) => {
    if (data.links[linkKey].is_folder || data.links[linkKey].slug === "home") {
      return;
    }
    const slug = data.links[linkKey].slug;
    let splittedSlug = slug.split("/");

    for (const locale of locales) {
      paths.push({ params: { slug: splittedSlug }, locale });
    }
  });
  return {
    paths: paths,
    fallback: false,
  };
}
    

Here, we are using the preview parameter from the context and we are fetching the draft content if we are inside the preview mode, and published content if not. We are also returning it from the getStaticProps, so that it is available in page function.

We've also changed the version to published inside the getStaticPaths while fetching the links for generating paths. This is because in the preview mode, the getStaticPaths function will have no effect, and we can access the draft links as well in the preview mode. This will also help us get only published links when in production. 

HINT:

Make sure to make the same changes inside the index.js file.

Let's try out a couple of things to see if our changes are visible. 

Add a new Story, save it but don't publish it. If you go to the preview URL {1}, you will be able to see the story.  

Unpublished Story in Preview Mode
1

Unpublished Story in Preview Mode

However, if you exit the preview using the exit URL {1}, you will see a 404 page. This means if a new page is added but not published, it will only be available in preview mode.

Unpublished Stoy after exiting preview mode
1

Unpublished Stoy after exiting preview mode

Similarly, let's go to any published story and switch to the preview URL. You will see that we are able to edit content now. Let's edit something and hit save. Do not publish the story. 

Preview Mode

Preview Mode

Now, if we exit the preview, we only see the published content and not the change we made (or the draft content). This means that we are loading different versions as well.

Exited preview mode

Exited preview mode

Now, let's deploy the website.

Deployment 

We are going to use Vercel for our deployment. If you don't have an account, you can create a new one for free. You can choose any provider of your choice for the deployment.

Before deploying, let's set up our environment file as well. This is needed because the access token shouldn't be present in the code. For this, create a new file and name it .env.

HINT:

You can read more on the environment variables here and set it up according to your requirements.

After creating the file, add your access token like this - NEXT_PUBLIC_STORYBLOK_ACCESS_TOKEN = <YOUR_ACCESS_TOKEN>

Prepending NEXT_PUBLIC_ will make our access token available to the browser as well. Most of our logic to get the data works on the server side but we need to make it available to the browser as we are doing client-side data fetching while getting all articles in the blog home with the useEffect hook. If your code doesn't make any request to Storyblok's API on the client side, you don't need to add NEXT_PUBLIC_ to your environment variable. You can now push the code to a git provider, like Github. Let's move to the deployment now.

After logging into Vercel, you should land on the dashboard that shows an overview of all the projects you have. We need to create a new project for this. Click on Add New button next to the search bar, and select project. You should be able to import the project from your git provider. After importing, we will just need to add our environment variable {1} and click on Add button {2}

Configuring Vervel
1
2

Afterwards, click on deploy, and your website should be built and deployed soon. Once it is deployed, you will see the screen below.

If you click on the website preview {1}, you will be taken to the deployed URL, or if you click on Continue to Dashboard {2}, you will be taken to the dashboard with the project tab selected and you will be able to see the URL over there as well under domains, along with other details.

1
2

You should be able to access your website with that URL now. Congratulations, you just deployed your full-blown, multilingual website made with Next.js and Storyblok. 

Now we can add the preview URLs inside our Visual Editor. Let's make this as a default location {1}.  Add a dev URL as  https://localhost:3010/ {2} so we can still see how it works in development. We can now replace localhost with our deployed link for our preview {3} and exit-preview {4}. We can even keep the localhost URLs and add these as new ones. Once the links are added {5}, make sure to hit save {6}.

Adding Preview URLs
1
2
3
4
5
6

Adding Preview URLs

The default link, when accessed anywhere including the Visual Editor will show the static and optimized version of your website. If we go inside the Visual Editor, we won't be able to click on blocks or see the live changes as the website is static and preview mode is not enabled. If we access the preview URL just like we did before, we will be able to see the dotted lines. Moreover, draft content instead of published. You can use the preview and exit-preview as we did before with localhost.

This setup will help the content editors do live edits, add new stories, and see the changes in real time without publishing the content to production when accessing the preview URL inside Storyblok. The deployed URL can also be used as your production URL. When it is being accessed by a website visitor, the preview mode is not enabled. And they will only see the published content. Anyone accessing it will not have the SECRET_TOKEN that is being used for the preview route, so no one will be able to enable the preview mode.

Vercel rebuilds and deploys the project automatically when you push any changes to the repository. With all this done, it would be great that as soon as anyone publishes a story, our website should be rebuilt and redeployed on Vercel. Right now, if you make any changes and publish the website, to see the changes we will need to redeploy the website from Vercel, which will build it again. However, doing this everytime a change is made, can be a bit time-consuming. Let's see how we can make this process seamless. 

Adding a deploy webhook

Storyblok allows you to trigger your webhooks very easily. You can trigger webhooks from Storyblok if you want to react inside any other services depending upon the events happening in Storyblok. Storyblok by default gives you the option to add webhooks for a few events. We will take a look at those in a bit.

You can even trigger webhooks manually from Storyblok by installing the Tasks app and configuring them independently.

HINT:

You can read more about webhooks and how they work with Storyblok here.

Vercel allows you to create deploy hooks which can be added to Storyblok for the story events. These deploy hooks are urls that allow you to trigger the deployment in Vercel. To generate a deploy hook, go to the Settings {1} of your project in Vercel and select git {2}. Inside the git tab, you will see there is a section for Deploy Hooks {3}. You can create a hook there by giving it a name {4} and specifying the branch {5}. Hit Create Hook {6} after filling in the details.

Vercel Deploy Hook
1
2
3
4
5
6

Vercel Deploy Hook

Once the webhook is created, you will get an option to copy the hook's URL. Let's now add it to Storyblok. Go to the Settings {1}, and select Webhooks {2}. This is the place where you can add webhook URLs for Storyblok events. Click on New Webhook {3} to add one.

Webhooks in Storyblok
1
2
3

Add a name for the webhook {1}, and then paste the URL of the copied webhook in Endpoint URL {2}. You can select the Triggers {3} here for which the webhook should be fired.

In this case, we want to rebuild our website whenever something is changed with a Story. Let's select all the Story events {4}.

1
2
3
4

And that's it, now whenever a story is published or changed, the project will be redeployed again automatically without any extra effort. With all this done, there is still a little thing we can do to make our setup even better.

Using Public Token for production 

You might have observed that we have exclusively been using the preview access token so far. Preview tokens can give you access to both draft as well as published content from Storyblok. It is recommended to use the public token for production environments. The public token will give the access to only the published version of the content. 

If you go to the settings of your space and click on the Access Tokens tab {1}, you will see a list of your access tokens along with their access levels. If you don't have a public access token, create one by clicking the Generate Button {2} keeping the access level as Public {3}. You can even give a name to the token {4}, this is optional. 

1
2
3
4

Now, we can make another deployment to our Vercel account where we use the public token while initializing Storyblok. The project setup and structure can remain the same, only the environment variable needs to get changed. After making this deployment you will see that if you try to access the preview mode by appending the preview route, you will get a server error with code 500. This is because as soon as we enable the preview mode, our code tries to fetch the draft content. However, since we are using the public token, we are going to get an error retrieving draft content. This means that your production URL will be even safer, as no one can access the draft content with that.

This deployment can be made according to the requirements. You can even create a new space, a new branch, or both with the different access token in the environment depending on your project setup. You can even use the Storyblok Pipelines app to create different environments.

HINT:

Storyblok CLI and management API can help if you're using multi space setup.

Now we get two different environments. One with the preview token and the other one with the public token. The one with the preview token can be used internally within the teams for editing content, reviewing the changes, and doing live edits. With the preview mode enabled it can also be shared with the stakeholders to show them the draft content. It can be used to access both versions of content. The environment with the public token can be used for the public, which will only show the published version to the audience.

Congratulations! Now, we are done with everything. You're now production ready.

Wrapping Up

In this tutorial, you saw how to add the Preview Mode to our Next.js and Storyblok website. We also saw how to switch to different content versions with the help of preview mode. We also deployed our website to different environments using different access tokens.

ResourceLink
Storyblok Next.js Ultimate Tutorialhttps://www.storyblok.com/tp/nextjs-headless-cms-ultimate-tutorial
Storyblok Technologies Hubhttps://www.storyblok.com/technologies
Next.js Technology HubStoryblok Next.js Technology Hub
Storyblok React SDKstoryblok/storyblok-react

Author

Chakit Arora

Chakit Arora

Chakit is a Full Stack Developer based in India, he is passionate about the web and likes to be involved in the community. He is a Twitter space host, who likes to talk and write about technology. He is always excited to try out new technologies and frameworks. He works as a Developer Relations Engineer at Storyblok.