How to Enable the Storyblok Visual Editor in Your Hybrid Astro Project
Enabling the Storyblok visual editor in your Astro project can streamline your content management process. Traditionally, this required deploying two versions of your site (SSG + SSR), but with Astroβs Hybrid mode, we can simplify things. Letβs walk through the steps to make this work with minimal fuss.
Prerequisites
Before diving into the tutorial, ensure you have the following set up to successfully enable the Storyblok Visual Editor in your hybrid Astro project:
An Astro project with an active astro-storyblok integration.
A global layout wrapper included in your template for a seamless editing experience.
1. Set Up Environment Variables
First, you need to define two environment variables in your astro.config.mjs
. This allows your project to use the necessary credentials without hardcoding them. Now add your STORYBLOK_SPACE_ID & SITE_LANG to your .env.
Also make sure Astro is in Hybrid mode.
experimental: {
env: {
schema: {
STORYBLOK_SPACE_ID: envField.number({
context: "server",
access: "public",
optional: false,
}),
SITE_LANG: envField.string({
context: "client",
access: "public",
default: "en",
}),
},
},
},
2. Create Your bridge component
create src/components/SbBridge.astro
<script>
import { loadStoryblokBridge } from "@storyblok/astro";
document.addEventListener("DOMContentLoaded", () => {
loadStoryblokBridge().then(() => {
const { StoryblokBridge, location } = window;
const storyblokInstance = new StoryblokBridge({
/* preventClicks: true, */
});
storyblokInstance.on(["published", "change"], (event: any) => {
if (!event.slugChanged) {
location.reload();
}
});
});
});
</script>
3. Edit Your Layout
Next, set up your project for Static Site Generation (SSG) and create or edit your layout.astro
. Here, youβll add bridge?: boolean;
to the props to enable the Storyblok bridge script on your CMS route.
---
import SbBridge from "../components/SbBridge.astro";
import { SITE_LANG } from "astro:env/client";
import { ViewTransitions } from "astro:transitions";
interface Meta {
title?: string;
description?: string;
image?: string;
}
interface Props {
meta?: Meta;
bridge?: boolean;
lang?: string;
}
const { bridge, meta, lang } = Astro.props;
---
<!doctype html>
<html lang={lang || SITE_LANG}>
<head>
<meta charset="UTF-8" />
<title>{meta?.title}</title>
<meta name="description" content={meta?.description} />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
{
bridge && (
<script
src="//app.storyblok.com/f/storyblok-v2-latest.js"
type="text/javascript"
is:inline
/>
)
}
{!bridge && <ViewTransitions />}
</head>
<body>
<header>...</header>
<slot />
<footer>...</footer>
</body>
</html>
{ bridge && <SbBridge /> }
4. Create a CMS Route
Now, add a route for your CMS at /src/pages/cms/index.astro
. This will handle incoming requests from Storyblokβs visual editor.
---
import { SITE_LANG } from "astro:env/client";
import { useStoryblokApi } from "@storyblok/astro";
import StoryblokComponent from "@storyblok/astro/StoryblokComponent.astro";
import type { ISbResult } from "@storyblok/astro";
import Layout from "../../layouts/Layout.astro";
export const prerender = false;
const api = useStoryblokApi();
const slug = Astro.url.searchParams.get("url");
if (!slug) {
return Astro.redirect("/404");
}
/* SOME HELPER FUNCTIONS */
function getLangFromUrl(url: URL, locales: string[]) {
const [, lang] = url.pathname.split("/");
if (locales.includes(lang)) return lang as string;
return SITE_LANG;
}
function getDataUrl(url: URL, locales: string[]) {
const [, lang] = url.pathname.split("/");
let baseUrl = url.pathname;
if (locales.includes(lang)) {
baseUrl = url.pathname.replace(`/${lang}`, "");
}
return baseUrl;
}
const getStory = async (slug: string | undefined, lang: string | undefined) => {
const { data } = (await api.get(
`cdn/stories/${slug !== undefined ? slug : "index"}`,
{
version: "draft",
language: !lang || lang === SITE_LANG ? "default" : lang,
},
)) as ISbResult;
return data.story.content;
};
/* WE GET SUPPORTED LOCALES FROM THE SPACE DATA */
const { data } = (await api.get("cdn/spaces/me", {})) as SpaceObj;
const locales = [SITE_LANG, ...data.space.language_codes];
const storyUrl = new URL(slug, Astro.url.origin);
const lang = getLangFromUrl(storyUrl, locales);
const dataUrl = getDataUrl(storyUrl, locales);
/* Add your own Interface of your blok instead of any */
let blok: any | null = null;
try {
blok = await getStory(dataUrl, lang);
} catch (e) {
console.log("error", e);
}
---
<Layout bridge={true}
>{blok && <StoryblokComponent blok={blok} status="draft" />}
{
!blok && (
<div class="grid h-full min-h-svh w-full place-items-center">
<div class="text-center">
<h1 class="text-4xl">Page Not Found</h1>
<div>(or not saved)</div>
</div>
</div>
)
}
</Layout>
5. Implement Middleware for CMS Route
To ensure that this route only works within Storyblok, implement some middleware. This step restricts access and helps manage content securely.
import { defineMiddleware } from "astro:middleware";
import { STORYBLOK_SPACE_ID } from "astro:env/server";
export const onRequest = defineMiddleware(async (context, next) => {
const { url, redirect } = context;
/* EXPOSE CMS ROUTE ONLY FOR THE CMS */
if (url.pathname.startsWith("/cms")) {
const sbSpaceId = url.searchParams.get("_storyblok_tk[space_id]");
if (!import.meta.env.DEV && sbSpaceId !== STORYBLOK_SPACE_ID.toString()) {
return redirect("/404");
}
}
return await next();
});
Once youβve pushed these changes to your live site, the visual editor should be fully operational in your Storyblok project. you can set the visual editor link to:
https://your-website.com/cms/?url=
This integration allows for a smoother workflow and enhanced content management, making it easier to edit and preview your site directly.
Need help with your Storyblok Astro project?
Get premium support to make the most of your integration and streamline your workflow.