The short version
Install @astrojs/rss and storyblok-js-client, map each story's full_slug to a link plus title, pubDate and description, and return the feed from an rss.xml.js endpoint. A Storyblok webhook can trigger a new deploy after publication.
RSS never went away. Feed readers, podcast apps, newsletter tools, and AI agents still consume feeds. That makes an RSS feed a low-maintenance distribution channel. In Astro with Storyblok, it takes a single endpoint.
The structure of this tutorial has barely changed since it was written: one helper client, one rss.xml endpoint and a feed generated at build time. The syntax has changed: Astro endpoints now export a GET function instead of get(), and the Storyblok CMS Delivery API v2 wraps responses in an envelope. The code below reflects both changes.
Prerequisites
Before you begin, make sure you have the following:
- Node.js (the current LTS version)
- An Astro project (Astro 7 is current; version 4 or later works with this pattern)
You also need a Storyblok space containing some stories, usually organized in a blog/ folder.
Install dependencies
From within the Astro project folder, run:
npm install @astrojs/rss storyblok-js-clientSetting up environment variables
Create a .env file in your Astro project with the following variables:
STORYBLOK_TOKEN="your-storyblok-api-token"
STORYBLOK_VERSION="published"Replace your-storyblok-api-token with a token from your Storyblok space. The token is only read at build time by the endpoint, so it never ships to the browser.
Creating a helper function
One shared client keeps the Storyblok connection logic out of your endpoints. Save this as src/library/sb.js:
import StoryblokClient from 'storyblok-js-client'
export const sb = new StoryblokClient({
accessToken: import.meta.env.STORYBLOK_TOKEN,
region: 'eu',
})
export const defaultConfig = {
version:
import.meta.env.STORYBLOK_VERSION === 'draft' || import.meta.env.DEV
? 'draft'
: 'published',
}This defaultConfig lets you see draft content during development and published content in the build without changing the code.
Creating the RSS feed endpoint
Create rss.xml.js in your src/pages folder. Double-check that you configured a site in astro.config.mjs — @astrojs/rss needs it to build absolute links. Then add:
import rss from '@astrojs/rss'
import { sb, defaultConfig } from '../library/sb'
export async function GET(context) {
const stories = await sb.getAll('cdn/stories', {
starts_with: 'blog/',
sort_by: 'first_published_at:desc',
...defaultConfig,
version: 'published',
})
// Replace 'blog_post' with the component name used by your article model.
const items = stories
.filter((story) => story.content?.component === 'blog_post')
.map((story) => ({
link: `/${story.full_slug === 'home' ? '' : story.full_slug.replace(/\/$/, '')}`,
title: story.content?.title ?? story.name,
pubDate: new Date(story.first_published_at ?? story.created_at),
description: story.content?.description,
}))
return rss({
title: 'My Blog',
description: 'My blog description',
site: context.site,
items,
customData: '<language>en-US</language>',
trailingSlash: false,
stylesheet: '/pretty-feed-v3.xsl',
})
}A few choices here deserve an explanation. The optional chaining in story.content?.title matters more than it looks: folder-only stories or stories with a different content model may not have a title field, and one unhandled undefined can break the whole build. The feed explicitly requests published stories; first_published_at ?? created_at provides a fallback date. Set the component filter to your actual article model and verify that each full_slug resolves to a public page. trailingSlash: false should match your site’s URL convention, because feed links that differ from canonical URLs by a slash cause unnecessary redirects.
The stylesheet line refers to the Pretty Feed v3 stylesheet, which goes in your public folder. It turns the raw XML into a readable page for people who click the feed link instead of showing them a wall of angle brackets.
On the v2 Delivery API: a raw fetch returns an envelope with stories, cv, rels and links. The SDK’s getAll flattens the envelope and paginates for you. If you fetch manually instead, pass cv as a query parameter — it is the space’s cache version, and it is what makes Storyblok’s CDN serve fresh content immediately after a publish rather than a stale copy.
Conclusion
With one helper file and one endpoint, your site serves a complete RSS feed that regenerates on every deploy. It contains your Storyblok stories and their dates, is readable in a browser through the stylesheet and works in feed readers. Connect a Storyblok webhook to your deploy provider and publishing a story will update the feed without a manual rebuild.
The same fetch pattern also powers localized sitemaps for Astro and Storyblok. Both endpoints can share the same sb.js client.

