The short version

Install @astrojs/rss and storyblok-js-client, create one shared client, map each story to a link, title, pubDate and description, and return the feed from an rss.xml.js endpoint. Add an auto-discovery link to your layout. A Storyblok webhook can trigger a new deploy after publication.

RSS never went away. Feed readers, podcast apps, newsletter tools, and AI agents still read feeds. That makes an RSS feed a low-maintenance way to share your posts. In Astro with Storyblok, an Astro RSS feed takes a single endpoint.

In four steps:

  1. Install @astrojs/rss and storyblok-js-client.
  2. Create one shared Storyblok client in src/library/sb.js.
  3. Return the feed from a src/pages/rss.xml.js endpoint.
  4. Add an auto-discovery link to the head of your layout.

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(), as the RSS recipe in the Astro docs shows. The Storyblok CMS Content Delivery API v2 wraps responses in an envelope, which Storyblok’s own RSS tutorial covers in more detail. 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:

bash
npm install @astrojs/rss storyblok-js-client

Setting up environment variables

Create a .env file in your Astro project with the following variables:

bash
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:

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.

How do you create the RSS endpoint in Astro?

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:

js
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. It makes Storyblok’s CDN serve fresh content right after a publish instead of a stale copy.

Each item here carries a summary, not the full post. @astrojs/rss also accepts a content field with the full post as HTML, as the Astro docs on full post content explain. With Storyblok you would first render the rich text field to HTML. Relative links and image paths are not supported there, so use absolute URLs. For most blogs, a summary is enough to send readers to the post.

How do readers and apps find your feed?

A feed only helps when people can find it. Add one link element to the head of your shared layout. Feed readers and browser extensions then detect the feed on any page, without the visitor knowing its URL.

astro
<link
  rel="alternate"
  type="application/rss+xml"
  title="My Blog"
  href={new URL('rss.xml', Astro.site)}
/>

Use the same title as the feed itself, and keep the href absolute so it works from every route. This site does the same: every page links to its own feed at /en/rss.xml. The content here lives in Git, not in Storyblok; I explain that choice in a website without a CMS. The endpoint and the link tag work the same way.

Does the feed update when you publish in Storyblok?

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. Prefer to have endpoints like these designed and tested for your project? The Astro development service describes how I take on that work.