⚠️Draft Content

Storyblok Raises $80M Series C - Read News

What’s the True Total Price of Enterprise CMS? Find out here.

Skip to main content

Create a Preview Environment for Your Nuxt 3 Website

Try Storyblok

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

This tutorial will explore deploying our Nuxt website on Netlify using two different approaches. First, as a static site, taking full advantage of the Jamstack approach and Nuxt 3, seeing how to generate a preview for our web by creating a Nuxt plugin to refresh the content, enabling live editing on our statically deployed environment. Then, we will see a different approach with a separate deployment for our preview and production environments to make it more optimized and efficient (recommended).

Hint:

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

Requirements

This is the last part of the Ultimate Tutorial Guide for Nuxt. The previous part shows how to add and manage multiple languages in Storyblok and Nuxt. Check out the previews parts!

Hint:

We will use the code from the previous tutorial as a starting point. You can find it in this StackBlitz.

1st Approach: Full Static with Preview Mode

Nuxt 3 has the possibility to deploy a pre-rendered application using Static Site Generation (SSG) with ssr: true. By running the command nuxi generate Nitro will pre-render the routes of our application at build time. Check Static Hosting in the official Nuxt 3 docs for more details.

Let’s prepare our project to use this rendering method while being able to preview changes in our Headless CMS.

Hint:

For this workaround, we will only need the Storyblok Preview Access Token.


Adding Preview Mode

In Nuxt 3, to refresh the static content on a page when we are editing inside the Storyblok Visual Editor, or showcasing the draft content to an external reviewer, we shall set up a plugin. This preview plugin will allow retrieving the draft content while keeping the published version at build time, bypassing the Static Site Generation.

Note:

This workaround was inspired by the suggestion of Jonathan Doelan at Nuxt 3 Preview mode discussion.

Inside the plugins folder, add a preview.js file with the following code:

plugins/preview.js
        
      export default defineNuxtPlugin((nuxtApp) => {
  const route = useRoute();
  const preview = route.query?._storyblok || false;

  if (preview) {
    nuxtApp.hook('page:finish', () => { refreshNuxtData() });
  }

  return { provide: { preview } };
});
    

This plugin checks if the website is rendered inside Storyblok by reading the query parameter _storyblok (line 3). If it’s present and the page has finished loading, it will automatically re-fetch the data, even if it is static (line 5).

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


Loading the Draft Version only in Preview Mode

Once we add the preview mode, we can use $preview, available inside useNuxtApp(), to fetch different versions of data: draft for the preview mode and published otherwise.

Hint:

The Preview Access Token has access to both types of content draft and published .

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

pages/[...slug].vue
        
      
<script setup>
const { $preview } = useNuxtApp()
const { slug } = useRoute().params
const url = slug && slug.length > 0 ? slug.join('/') : 'home'

// API options
const version = $preview ? 'draft' : 'published'
const { locale } = useI18n()
const resolveRelations = ['popular-articles.articles']

// Full Static with refresh approach
const { data: story, pending } = await useAsyncData(
  `${locale.value}-${url}`,
  async () => {
    const { data } = await useStoryblokApi().get(`cdn/stories/${url.replace(/\/$/, '')}`, {
      version,
      language: locale.value,
      resolve_relations: resolveRelations
    })
    return data?.story
  },
);

// Load the bridge in preview mode
onMounted(() => {
  if ($preview && story.value && story.value.id) {
    useStoryblokBridge(
      story.value.id,
      (evStory) => story.value = evStory,
      {
        resolveRelations,
      }
    );
  }
});
</script>

<template>
  <StoryblokComponent v-if="pending === false && story" :blok="story.content" />
</template>
    

Here, we are using the $preview parameter from useNuxtApp() to decide when to fetch the draft or the published content by providing the version parameter to the request (line 7 & 16). As you can see, we are only loading the bridge if $preview is enabled, to avoid extra code in production (line 26).

Hint:

In order to rerender the nested components, is important to listen the pending value coming from the useAsyncData in the conditional (line 39), otherwise we won’t be able to see the draft content.

Now, let's deploy the website.

Deployment

We are going to use Netlify for our deployment. If you don't have an account, you can create a new one for free. But of course, feel free to choose any static-friendly provider that you like 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 .env file in the root of your project, and add the access token: STORYBLOK_ACCESS_TOKEN=<YOUR_ACCESS_TOKEN>. Then, in your nuxt.config.js file, update the module with the following code:

nuxt.config.js
        
      
modules: [
  '@storyblok/nuxt',
  // other modules
],
storyblok: {
   accessToken: process.env.STORYBLOK_ACCESS_TOKEN, // New .env variable
   use: [apiPlugin]
},
    

Now push the code to a git provider, like Github, and let’s deploy it.

After logging into Netlify, you should land on the dashboard {1} that shows an overview of all the projects you have. We need to create a new project, click on Add new site {2} button in the Sites section, and select Import an existing project {3}.

Adding a new static deployment at Netlify
1
2
3

Adding a new static deployment at Netlify

Then, follow the steps:

  1. Connect to git provider: GitHub (or equivalent)
  2. Select repository: nuxt-ultimate-tutorial (your frontend repo)
  3. Configure site and deploy: After importing the repo, we will need to configure the commands and variables needed to pre-render our site. Add the npm run generate command {1} and dist as the publish directory {2}. Finally you will need to include the environment variable {3} by clicking the New variable button {4} and adding the respective key and value {5}.
Configure the deployment commands and variables inside Netlify
1
2
3
4
5

Configure the deployment commands and variables inside Netlify

Afterwards, click on deploy, and your website should be built and deployed soon. Once it is deployed, you will see a small screenshot, a random Netlify generated URL and a published green tag.

Congratulations, you just deployed your full-blown multilingual website made with Nuxt 3 and Storyblok. 

Adding a preview URL

Now, we can add the URL for our production environment to our Visual Editor. Inside the Visual Editor, click on Change URL {1} and on Add or change preview URLs {2}.

Add or change preview URLs in the Storyblok space
1
2

Add or change preview URLs in the Storyblok space

This will take us to the Visual Editor Settings {1}. Here, we will add a new preview URL to our just deployed environment: Nuxt 3 Prod {2}. The preview URL will be https://nuxt-ultimate-tutorial-ssg.netlify.app {3}, but feel free to add the production URL as default, once you’re ready {4}. Then hit the Save button {5}.

Hint:

You can always customize the Netlify random generated URL or add your own domain.

Adding deployed Preview URL
1
2
3
4
5

Adding deployed Preview URL

If we access the newly added preview URL inside the Visual Editor, we will be able to see the dotted lines. Moreover, draft content instead of published as in development. 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.


Although this is a possible solution, we always recommend having two separate environments for Preview and Production, so as not to overload your hosting provider's usage and have a clear separation of concerns. Let's see how this can be done.

2nd Approach: Preview and Production separate environments (recommended)

In an ideal world, it would be great to be able to use the same code but have two different deployments on our trusted hosting, one for previewing changes, and one for the end user.

Luckily, with frameworks like Nuxt, and its versatility to switch from one rendering mode to another, with a couple of environment variables we can control how we want to render the page and, therefore, be able to display draft or published content depending on those variables.

Hint:

For this approach, we will need two tokens, Storyblok Preview & Public Access Token.

Setting up the Preview Mode

In this type of approach we don’t need a client plugin to refresh our static site as we will directly serve a Client Side Rendering (CSR) application for the preview mode.

The only thing we have to do is to create a new environment variable called NUXT_PUBLIC_NODE_ENV and give it the value preview. This variable will be the one we will use to load the draft or published content and define what kind of rendering we want.

Adding the Rendering method conditional

To build our application as Single Page App (SPA) when the NUXT_PUBLIC_NODE_ENV doesn’t have the value production, add this line in nuxt.config.js:

nuxt.config.js
        
      export default defineNuxtConfig({
  ssr: process.env.NUXT_PUBLIC_NODE_ENV === 'production' ? true : false,
  // Leave here other things like: css, modules, i18n, nitro
})
    

Loading the draft content in the Preview environment

Now, we need to tell the [...slug].vue page when to load draft or published data. We can do it by using the same environment variable, we just need to add it as part of the public runtimeConfig in nuxt.config.js:

nuxt.config.js
        
      export default defineNuxtConfig({
  // Leave here other things like: ssr, css, modules, i18n, nitro
  runtimeConfig: {
    public: {
      NODE_ENV: process.env.NODE_ENV
    }
  },
})
    

Once we have it available, we can override the […slug].vue with the following code:

pages/[…slug].vue
        
      <script setup>
const { slug } = useRoute().params
const url = slug && slug.length > 0 ? slug.join('/') : 'home'

const isPreview = useRuntimeConfig().public.NODE_ENV !== 'production'
const { locale } = useI18n()
const resolveRelations = ['popular-articles.articles']

const { data: story, pending } = await useAsyncData(
  `${locale.value}-${url}`,
  async () => {
    const { data } = await useStoryblokApi().get(`cdn/stories/${url.replace(/\/$/, '')}`, {
      version: isPreview ? 'draft' : 'published',
      language: locale.value,
      resolve_relations: resolveRelations
    })
    return data?.story
  },
)

if (!isPreview) {
  if (!story.value) {
    showError({ statusCode: 404, statusMessage: "Page Not Found" })
  }
}

onMounted(() => {
  if (isPreview && story.value && story.value.id) {
    useStoryblokBridge(
      story.value.id,
      (evStory) => story.value = evStory,
      {
        resolveRelations,
      }
    )
  }
})
</script>

<template>
  <StoryblokComponent v-if="pending === false && story" :blok="story.content" />
</template>
    

Surely you are thinking, it looks quite similar to the previous approach, and it is totally true, we add the same conditionals to load one type of content or another (line 13) and to load or not the bridge (line 28), the difference is that now we use the environment variable to determine which content version should be fetched (line 5).

Deploying the Preview and Production sites

Now that we have modified our code to work in both environments, we only have to deploy the project in Netlify for both cases:

Preview environment

We will need the Preview Access Token and activate the preview mode.

.env
        
      STORYBLOK_ACCESS_TOKEN='<PREVIEW_TOKEN>'
NUXT_PUBLIC_NODE_ENV='preview'
    

Prod environment

We will need the Public Access Token, which only have access to published content, and activate the production mode.

.env
        
      STORYBLOK_ACCESS_TOKEN='<PUBLIC_TOKEN>'
NUXT_PUBLIC_NODE_ENV='production'
    
Hint:

To create a Public Access Token, check our FAQ How to retrieve and generate access tokens

Both will use the npm run generate command {1} and the dist folder {2}:

Note:

For the preview instance, as is CSR it doesn’t matter if you use the npm run build or npm run generate command to build your site.

Build Settings for both Preview and Prod environments in Netlify
1
2

Build Settings for both environments in Netlify

Congratulations!! You have been able to deploy your production site and create an internal only environment to edit your pages and test them beforehand. Don't forget to add the Preview URL in the Settings of your Storyblok Space and you'll be ready for takeoff 🚀

Adding a Build Webhook

Netlify rebuilds and deploys the project automatically when you push any changes to the code, but what about when we publish our pages inside the Visual Editor? Right now, if you make any changes and publish the content, to see them you will need to redeploy the website from Netlify. However, doing this every time, can be annoying. Let's see how we can make this process seamless.

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

Hint:

You can read more in the Storyblok Webhooks docs.

Netlify allows you to create build hooks which can be added to Storyblok for the story events. These build hooks are URLs that allow you to trigger the deployment in Netlify. To generate a build hook, go to the Site configuration {1} of your project and select Build & deploy {2}. If you scroll inside the Continuous deployment section {3}, you will find Build Hooks {4}. You can create a hook there by giving it a name {5} and specifying the branch {6}. Hit Save {7} after filling in the details.

Adding a new Build Hook on Netlify
1
2
3
4
5
6
7

Adding a new Build Hook on Netlify

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.

Adding a Webhook in Storyblok v2
1
2
3

Adding a Webhook in Storyblok v2

Add a name for the webhook {1}, and then paste the URL of the Netlify’s build hook 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} and click the Save button {5}.

Defining a new Webhook in Storyblok
1
2
3
4
5

Defining a new Webhook in Storyblok

And that's it, now whenever a story is published, moved, unpublished or deleted, the project will be redeployed and rebuilt again automatically without any extra effort.

Author

Alba Silvente

Alba Silvente

Alba, aka Dawntraoz, is a DevRel Engineer at Storyblok. She writes about frontend development on her personal blog, dawntraoz.com, and she is working hard on open-source projects to create more value for the web community.