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 Astro Website

Try Storyblok

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

In this tutorial, we are going to learn the essential steps to prepare our site for deployment. By default, Astro functions as a static site generator, generating all pages during the build process. However, previewing the site within the Storyblok Visual Editor requires a different approach than static.

HINT:

This tutorial concludes the Ultimate Astro Tutorial series. To fully grasp the content, it's essential to go through all the preceding parts.

To address this challenge, we’re opting for a dual deployment strategy. One instance employs Astro SSR, ensuring smooth integration with the Storyblok Visual Editor for previews. The second version focuses on transforming the site into a static version optimized for production.

For this tutorial, we’ll be using Vercel for deployment. It's worth noting that this deployment method applies to any hosting provider supporting both SSR and SSG deployment for Astro. You can find a comprehensive list of such providers here.

Live demo:

If you’re in a hurry, have a look at our live demo on Vercel! Alternatively, you can explore or fork the code from the Astro Ultimate Tutorial GitHub Repository.


Preparing the codebase for a multi-strategy Deployment

First, let's set up an environment variable. Based on this variable, we'll decide whether to use SSR or SSG. Additionally, we currently always fetch the draft version of the story from Storyblok in our code. We can make this dynamic, too.

.env
        
      STORYBLOK_TOKEN=xxxxx
STORYBLOK_IS_PREVIEW=yes
    
src/utils/isPreview.js
        
      export default function isPreview() {
  return import.meta.env.STORYBLOK_IS_PREVIEW === 'yes'
}
    

To handle SSR, we introduce a new variable, STORYBLOK_IS_PREVIEW. If it's set to yes, we use Astro’s SSR mode and fetch the draft data from Storyblok. For any other value, we do the opposite, rendering the site statically and fetching published data.

Next, in our catch-all route, we face an issue. While getStaticPaths works for SSG, it’s ignored in SSR, leading to errors. The problem arises from differences in fetching data between SSR and SSG.

To bridge this gap, we create a utility function that provides the same information for SSR as we get in SSG.

src/utils/parseUrl.js
        
      import { languages } from './langs'

export default function parseUrl(url) {
  //converting the current url to an array based on '/'
  let urlToArray = url?.split('/')
  //Setting the fallback language to be english
  let defaultLang = 'en'
  //Checking if current url contains a known language
  let isKnownLang = languages.some((l) => l === urlToArray?.[0])
  //setting current language based on above
  let currentLang = url && isKnownLang ? urlToArray[0] : defaultLang
  // removing language from the url and only keeping the slug
  let slug = url
    ? isKnownLang
      ? urlToArray?.slice(1)?.join('/') || undefined
      : urlToArray?.join('/')
    : undefined

  //Same logic for generating the lang switch as we have in getStaticPaths
  let langSwitch = {}
  languages.forEach((lang) => {
    langSwitch = {
      ...langSwitch,
      [lang]: lang === 'en' ? `/${slug ?? ''}` : `/${lang}/${slug ?? ''}`,
    }
  })
  //finally returning the same three variables we also get from getStaticPaths
  return { language: currentLang, slug, langSwitch }
}

    

In the function above, we parse the full URL to extract both the language and the slug. As mentioned in our earlier sections, Storyblok considers language as a parameter, not a part of the URL. Additionally, we'll generate the langSwitch, similar to what we did in the getStaticPaths function.

Additionally, let's relocate all the logic from the getStaticPaths function in the [...slug].astro file to a separate utility file. This will ensure our [...slug].astro file remains clean and organized.

src/utils/generateStaticPaths.ts
        
      import { useStoryblokApi } from '@storyblok/astro'
import isPreview from './isPreview'
import { languages } from './langs'

export default async function generateStaticPaths() {
  const storyblokApi = useStoryblokApi()
  const links = await storyblokApi.getAll('cdn/links', {
    version: isPreview() ? 'draft' : 'published',
  })
  let paths = []
  links
    .filter((link) => !link.is_folder)
    .forEach((link: { slug: string }) => {
      languages.forEach((language) => {
        //This slug will be used for fetching data from storyblok
        let slug = link.slug === 'home' ? undefined : link.slug
        //This will be used for generating all the urls for astro
        let full_url = language === 'en' ? slug : `${language}/${slug ?? ''}`
        //This will let us change the url for diffrent versions
        let langSwitch = {}
        languages.forEach((lang) => {
          langSwitch = {
            ...langSwitch,
            [lang]: lang === 'en' ? `/${slug ?? ''}` : `/${lang}/${slug ?? ''}`,
          }
        })
        paths.push({
          props: { language, slug, langSwitch },
          params: {
            slug: full_url,
          },
        })
      })
    })
  return paths
}
    

Now, let's modify our […slug].astro file to handle both SSR and SSG. We use the utility functions we just created to make it happen.

src/pages/[...slug].astro
        
      ---
import { useStoryblokApi } from '@storyblok/astro'
// @ts-ignore
import StoryblokComponent from '@storyblok/astro/StoryblokComponent.astro'
import BaseLayout from '../layouts/BaseLayout.astro'
import parseUrl from '../utils/parseUrl'
import isPreview from '../utils/isPreview'
import generateStaticPaths from '../utils/generateStaticPaths'

export async function getStaticPaths() {
  //We have moved all the code to a generateStaticPaths() function.
  return await generateStaticPaths()
}
const params = Astro.params
let props = isPreview() ? parseUrl(params?.slug) : Astro.props
const { slug, language, langSwitch } = props
const storyblokApi = useStoryblokApi()
const { data } = await storyblokApi.get(
  `cdn/stories/${slug === undefined ? 'home' : slug}`,
  {
    version: isPreview() ? 'draft' : 'published',
    resolve_relations: ['popular-articles.articles'],
    language,
  }
)
const story = data.story
---
<BaseLayout langSwitch={langSwitch} language={language}>
  <StoryblokComponent language={language} blok={story.content} />
</BaseLayout>
    

Lastly, our AllArticle.astro component, needs a similar adjustment.

src/storyblok/AllArticle.astro
        
      ---
import { storyblokEditable, useStoryblokApi } from '@storyblok/astro'
import ArticleCard from '../components/ArticleCard.astro'
import isPreview from '../utils/isPreview'
const { blok, language } = Astro.props
const storyblokApi = useStoryblokApi()
const { data } = await storyblokApi.get(`cdn/stories`, {
  version: isPreview() ? 'draft' : 'published',
  starts_with: 'blog/',
  is_startpage: false,
  language,
})
const articles = data.stories
---
...
    

Next, let's install the Vercel provider for Astro for deployment. You can follow this guide from Astro official docs to do this.

Now let's modify the astro.config.mjs a bit more to make it fully dynamic so we can work from one codebase and deploy different instances as we need.

astro.config.mjs
        
      import { defineConfig } from 'astro/config';
import storyblok from '@storyblok/astro';
import { loadEnv } from 'vite';
import tailwind from '@astrojs/tailwind';
import basicSsl from '@vitejs/plugin-basic-ssl';
import vercel from "@astrojs/vercel/serverless";
const env = loadEnv('', process.cwd(), 'STORYBLOK');

// https://astro.build/config
export default defineConfig({
  integrations: [storyblok({
    accessToken: env.STORYBLOK_TOKEN,
+    bridge: env.STORYBLOK_IS_PREVIEW === 'yes',
    components: {
      page: 'storyblok/Page',
      feature: 'storyblok/Feature',
      grid: 'storyblok/Grid',
      teaser: 'storyblok/Teaser',
      hero: 'storyblok/Hero',
      config: 'storyblok/Config',
      'popular-articles': 'storyblok/PopularArticles',
      'all-articles': 'storyblok/AllArticles',
      article: 'storyblok/Article'
    }
  }), tailwind()],
+  output: env.STORYBLOK_IS_PREVIEW === 'yes' ? 'server' : 'static',
  ...(env.STORYBLOK_ENV === 'development' && {
    vite: {
      plugins: [basicSsl()],
      server: {
        https: true
      }
    }
  }),
+  adapter: vercel()
});
    

Here is what we configured above:

  1. Enable Storyblok bridge only in the preview mode
  2. In preview mode, we are going to use server and static for production.
  3. Load the basicSsl only on local development (Optional)
learn:

Storyblok bridge is vital for the functionality of the Storyblok Visual Editor. To make it work, the Storyblok SDK adds extra information to HTML elements. This is ideal for the preview environment, but it's recommended to disable it for the production site. For more in-depth information, you can start your exploration here.

Deploying the preview version on Vercel

Begin by creating a new project in Vercel and importing the Git repository.

Create new project in Vercel

Create new project in Vercel

After completing this step, add your environment variables and proceed to deploy.

Add env variables in your vercel project

Add env variables in your vercel project

That's it! We have now successfully deployed the preview version of our Astro site. We will use this deployment URL in Storyblok to preview our website and leverage Storyblok's Visual Editor.

Deploying the production version on Vercel

For this step, follow the same process as mentioned above. Create a new project in Vercel and select the same Git repository. However, in the environment variables section, make sure to STORYBLOK_IS_PREVIEW to NO.

Add env variables for your Vercel project

Add env variables for your Vercel project

This process will deploy our Astro site statically, allowing us to experience the lightning-fast speed that comes with a static site.

Configuring the rebuild trigger for the production deployment

Now that we have successfully deployed our production site statically, you might wonder what happens when you create a new story in Storyblok or make changes to your existing pages.

Not to worry - Storyblok provides Webhooks that we can utilize to listen for changes and trigger a rebuild on Vercel.

Let's navigate to the project settings in Vercel.


Project settings in Vercel

Project settings in Vercel

  1. Add a hook name
  2. Type the branch you want to build the trigger
  3. Click Create Hook.

After clicking on 'Create Hook,' it will provide us with a link as shown below. Let's copy this link and go to our Storyblok Space.

Create Deploy Hook in Project settings in Vercel

Create Deploy Hook in Project settings in Vercel

Navigate to the Webhook section in the settings of your Storyblok Space.

Create Webhook Hook in Storyblok Space

Create Webhook Hook in Storyblok Space

In this step, provide a name for the webhook and paste the copied URL from Vercel into the Endpoint URL field. Then, select all the events for which you want the build to be triggered

That's it! Your pipeline setup for the project, from preview to production, is now complete.

learn:

If you wish to explore further details about all the webhook events provided by Storyblok, you can delve deeper by clicking here.

Alternative to Webhooks?

Wondering about alternatives to webhooks? If you prefer not to trigger the build based on webhook events, you can configure our Tasks App to initiate the build manually.

Install Task Manager App in Storyblok

Install Task Manager App in Storyblok

After installing the Task-Manager app in your space, you can create a new task and paste the link copied from Vercel into the Webhook field, following the line highlighted in the image below.

Create new task in Task Manager App in Storyblok

Create new task in Task Manager App in Storyblok

After saving the task, you have the flexibility to manually trigger the build whenever the need arises.

Trigger task in Task Manager App in Storyblok

Trigger task in Task Manager App in Storyblok

Conclusion

Congratulations! You have successfully acquired the skills to develop a comprehensive Astro website using Storyblok. Throughout this tutorial, you have learned the essential steps to prepare our Astro site for deployment.

Author

Dipankar Maikap

Dipankar Maikap

Dipankar is a seasoned Developer Relations Engineer at Storyblok, with a specialization in frontend development. His expertise spans across various JavaScript frameworks such as Astro, Next.js, and Remix. Passionate about web development and JavaScript, he remains at the forefront of the ever-evolving tech landscape, continually exploring new technologies and sharing insights with the community.