DEV Community

Jonas Gierer
Jonas Gierer

Posted on

Astro + Storyblok: SSR preview for instant visual editing

Storyblok is one of the most popular new content management systems. Its visual editing and component-based workflow are intuitive for editors, while also being easy for developers to work with.

While its @storyblok/astro integration with the Astro framework seems fairly straight-forward at first, getting the most out of both Storyblok's visual editor and Astro's static site generation is not as simple.

The Problem

Since Astro is (by default) a static site generator, which generates all pages at build time, editing content in Storyblok will not give your editors the instant feedback they might expect: Even if your Storyblok webhooks are set up correctly, it can take several minutes for the entire project to be rebuilt before the updated content appears. This makes for a very awkward and unintuitive editing experience.

One way to solve this problem is to use Astro in SSR mode. This uses a Nodejs server or serverless functions to render pages whenever they are requested, rather than at build time. This means pages will update as soon as editors change their content. Unfortunately this also means that you'll lose all benefits of a static site, such as the significantly faster page load times and lower operating costs.

The Solution

In order to get the best of both worlds, we'll want to set up two separate deployments: One that uses Astro's default SSG mode for your users to visit, and one SSR environment which is used as the preview site in Storyblok. This way, editors get instant feedback while editing content, but users will still get a fast, static site.

Assuming you followed the storyblok-astro Getting Started guide, here is a step-by-step guide to enable this dual environment:

1. Install Astro SSR adapter of your choice

Astro supports several platforms for server-side rendering. For this guide I will use Vercel's serverless functions. Please refer to the Astro docs for other platforms.

npx astro add vercel
Enter fullscreen mode Exit fullscreen mode

2. Conditionally enable SSR mode

This is where the magic happens: Based on an environment variable, we switch between Astro's server or static mode. I chose to call the variable PUBLIC_ENV because Astro makes environment variables prefixed with PUBLIC_ available in client-side code, which might come in useful. We also conditionally load the SSR adapter since it's not needed in static mode.

// astro.config.{mjs,ts}

import vercel from '@astrojs/vercel/serverless';
// ...

export default defineConfig({
  // ...
  output: process.env.PUBLIC_ENV === 'preview' ? 'server' : 'static',
  adapter: process.env.PUBLIC_ENV === 'preview' ? vercel() : undefined,
  // ...
});
Enter fullscreen mode Exit fullscreen mode

3. Conditionally enable the Storyblok bridge

The Storyblok bridge is responsible for reloading the page when there are changes to Storyblok content, as well as making individual components clickable in the Storyblok editor. Since we will never open the production static site in Storyblok, we can reduce the bundle size a little by disabling the bridge in production.

// astro.config.{mjs,ts}

export default defineConfig({
  // ...
  integrations: [
    storyblok({
      bridge: process.env.PUBLIC_ENV !== 'production',
      // ...
    }),
    // ...
  ],
  // ...
});
Enter fullscreen mode Exit fullscreen mode

4. Create separate build scripts

In order to build the two different deployments, we'll create two build scripts which set the PUBLIC_ENV variable to the respective value. For the sake of completeness, we'll also set the variable to development when using the dev server.

// package.json

"scripts": {
  "dev": "PUBLIC_ENV=development astro dev",
  "build:production": "PUBLIC_ENV=production astro build",
  "build:preview": "PUBLIC_ENV=preview astro build",
  // ...
},
// ...
Enter fullscreen mode Exit fullscreen mode

5. Deploy the two sites

Next, we'll create two separate deployments which use the different build scripts. In my case, I'll create two Vercel projects, connect both to the project's GitHub repo and set the Build Command under Settings > General > Build & Development Settings to npm run build:production for the static production site and to npm run build:preview for the SSR preview site. You should be able to do the same for Netlify, Cloudflare or similar providers.

6. Use webhooks to trigger production deploys

Now all that's left to do is to set up webhooks between Storyblok and your hosting provider so that the production site is rebuilt whenever content is published.

First, create a webhook in your production site's admin interface. For Vercel, I did this under Settings > Git > Deploy Hooks.

Then, copy the URL of the webhook you just created, and paste it in Storyblok under Settings > Webhooks > Story published & unpublished. Now your production site should be rebuilt whenever your editors publish or delete content!

Conclusion

This setup made Astro integrate with Storyblok much more nicely, and it is now one of my favorite framework-CMS pairings on the market. The awesome performance and developer experience of Astro static sites paired with the intuitive Storyblok visual editor is wonderful!

I hope this guide was helpful if you're building a site with these two technologies. Let me know if you have any questions or ideas for improvement!

Extras

There are a bunch of extra tips and tricks about Astro + Storyblok that I wanted to share, but since this guide is already getting long I decided to put them in a separate companion post, which you can find here: Astro + Storyblok: SSR Tips and Tricks. There I explain how to prevent drafts from showing up in your production site, disable search engine indexing of your preview site and more.

Top comments (10)

Collapse
 
manuelschroederdev profile image
Manuel Schröder

Thank you very much for this really great article. 👏
I'll add a bit more context to the @storyblok/astro readme to make it easier for new users to understand the need for two different deployments. 🙂

Collapse
 
jgierer12 profile image
Jonas Gierer

Thank you for the kind words and for making the Astro integration! It's really been a joy to work with :)

Collapse
 
manuelschroederdev profile image
Manuel Schröder

Thank you, very happy to hear that. :-)

Collapse
 
imjb1987 profile image
John Bell

Thanks for this article Jonas! This solution actually came off the back of an issue that I highlighted towards the end of last year and Manuel managed to make it so!

We have some Astro production builds coming up and I was planning to do this exact setup myself, but now I don't need to since it's all here!

Collapse
 
idiglove profile image
Faith Morante

Thanks so much for this! You rock

Is it necessary to create two Vercel projects?

Collapse
 
jgierer12 profile image
Jonas Gierer

Not necessarily, I just found it to be the easiest and cleanest way to do it. One possible way to do it with a single Vercel project is to make a git branch for the SSR build, and use the Vercel branch preview URL for the Storyblok editor. But then you need to remember to keep that branch in sync with the main branch.

Collapse
 
idiglove profile image
Faith Morante

One of the steps that we can add is to use useStoryblokApi()

---
const storyblokApi = useStoryblokApi()
const { data } = await storyblokApi.get("cdn/stories/test", {
  version: "draft",
})
const content = data?.story?.content ?? {}
---
  <main {...storyblokEditable(content)}>
    {
      content.body?.map((blok) => {
        return <StoryblokComponent blok={blok} />
      })
    }
  </main>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
preetamslot profile image
preetamslot

I found out that on new spaces the cdn/links endpoint is now paged by default.
so: 'const { data } = await storyblokApi('cdn/links');'
will now only get 25 results.

Collapse
 
imjb1987 profile image
John Bell

@jgierer12 have you tried this with dynamic routes at all eg [...slug].astro? We're having some problems trying to render them so wondered if you have attempted this.

Collapse
 
jgierer12 profile image
Jonas Gierer • Edited

I'm using it with the standard dynamic routes ([slug].astro without the rest parameter) and it works fine for me. I don't remember doing anything specific to make it work, it's pretty much just this with some minor changes. Maybe it could be an issue with the rest parameter?