
Short answer: Storyblok does not publish an RSS feed for you, but its Content Delivery API gives you everything a feed needs. Request the newest published stories from your blog folder with your public token, turn each story into an item with an absolute link, the first publication date and a summary field, escape the text, send the RSS content type and a cache validator and test the live address. Add a webhook so that the feed refreshes when an editor publishes, and connect the address to an auto-poster so that every new story is shared on your social networks.
Storyblok is a headless content management system. Editors work in a visual editor, and developers receive the content as JSON through an API and render it in whatever front end they like. That separation is a strength, but it has a side effect: nothing in the system knows what your website addresses look like, so nothing can produce a feed on its own. The feed has to be written in your front end, from the API data, and it must use the same addresses, the same slugs and the same dates as your pages. This guide builds a feed handler, explains the fields that matter and ends with a test routine.
A feed item needs a title, a permanent address, a publication date and a summary. In Storyblok, the title is the name of the story, the address comes from its slug and the date comes from the first publication, which the API supplies. The summary needs a field of its own in the blog post component, so that editors write it on purpose.
| Feed element | Source in Storyblok | Rule |
|---|---|---|
| Title | name of the story | Plain text |
| Link and identifier | full_slug and your site address | Absolute, https, never changes |
| Publication date | first_published_at | The first time the story went live |
| Description | a summary field in the component | One or two complete sentences |
| Image | an asset field in the component | Absolute address with a type |
Use the first publication date and not the date of the last edit. An editor who corrects a typo in an old story changes the modification date, and a feed that uses it would move the item to the top and make an auto-poster share the story again. The publication date stays where it was. The causes and cures of repeated posts are covered in why a feed auto-posts the same article twice.
The Content Delivery API returns stories as JSON. Ask for the published version, the blog folder only, the newest first and a limited number. Keep the token in an environment variable, and use the public access token, which can only read published content.
// app/feed.xml/route.js
const SITE = "https://example.com";
async function loadStories() {
const params = new URLSearchParams({
token: process.env.STORYBLOK_PUBLIC_TOKEN,
version: "published",
starts_with: "blog/",
is_startpage: "false",
sort_by: "first_published_at:desc",
per_page: "20",
});
const res = await fetch("https://api.storyblok.com/v2/cdn/stories?" + params, {
next: { revalidate: 300 },
});
if (!res.ok) throw new Error("Storyblok API returned " + res.status);
const data = await res.json();
return data.stories;
}The example uses a route handler of a React framework, but the logic is the same in any server that can call an API and return text. The API address, the parameter names and the field names can change, so check them in the documentation of the version that you use. The same approach for other headless systems is covered in the Sanity feed guide un the Contentful feed guide.
Text from a content system can contain ampersands, angle brackets and quotation marks, and any of them can break an XML document. Escape every value before it goes into the template.
const ENT = {
"&": "&" + "amp;",
"<": "&" + "lt;",
">": "&" + "gt;",
'"': "&" + "quot;",
"'": "&" + "apos;",
};
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ENT[c]);
function render(stories) {
const newest = stories.length ? new Date(stories[0].first_published_at) : new Date(0);
const items = stories.map((s) => {
const url = `${SITE}/${s.full_slug}/`;
return `<item>
<title>${esc(s.name)}</title>
<link>${esc(url)}</link>
<guid isPermaLink="true">${esc(url)}</guid>
<pubDate>${new Date(s.first_published_at).toUTCString()}</pubDate>
<description>${esc(s.content.summary)}</description>
</item>`;
}).join("\n");
return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel>
<title>Example Journal</title>
<link>${SITE}</link>
<description>News and guides from Example Journal.</description>
<lastBuildDate>${newest.toUTCString()}</lastBuildDate>
${items}
</channel></rss>`;
}Several details are deliberate. The date method of the language writes the format that the RSS specification expects. The channel date comes from the newest story and not from the clock, so that the file does not change when nothing was published. The identifier of each item is its permanent address. And the address is built from a fixed constant, so every link is absolute. If your site uses another folder structure, map the full slug to the real address of the page in one function, so that the feed and the website can never disagree.
The handler sets the content type, a cache lifetime and a validator, and it answers with a 304 status when the client already holds the current version.
import { createHash } from "node:crypto";
export async function GET(request) {
const xml = render(await loadStories());
const etag = '"' + createHash("sha1").update(xml).digest("hex") + '"';
const headers = {
"Content-Type": "application/rss+xml; charset=utf-8",
"Cache-Control": "public, max-age=300",
ETag: etag,
};
if (request.headers.get("if-none-match") === etag) {
return new Response(null, { status: 304, headers });
}
return new Response(xml, { status: 200, headers });
}The validator is a hash of the finished document, so it changes exactly when the content changes. A 304 answer saves bandwidth for you and for every tool that polls the feed; the mechanism is explained in the guide to ETags and conditional requests, and the status codes in the guide to feed status codes.
A feed that is cached for a few minutes is fine, but you can make it faster. Storyblok can call a webhook when a story is published or unpublished. Point that webhook at a small endpoint of your site that clears the cached copy of the feed, or rebuilds the site, and the new story appears within seconds. Keep a short cache lifetime as a safety net, because a webhook can fail without anyone noticing. Test the whole chain by publishing a draft story and watching the feed, since a stale feed delays every social post.
The public token returns only published content, which is exactly what a feed needs. Never use the preview token in a public feed, because it can return drafts, and a draft that leaks into the feed is shared on social networks before it exists on your website. Be careful with scheduled publishing too: a story that is scheduled for tomorrow is not published yet, and it appears in the API when the moment arrives. If you import old stories, keep their original dates in a field of your own, because a feed full of items with today’s date makes an auto-poster share your whole archive in one afternoon. For the effect of old dates see the guide to feed order and backdated posts.
Auto-posters build the best social posts from a feed that gives them a short summary and a picture. Make the summary a required field, so that no story reaches the feed without one, and write complete sentences instead of cutting the body automatically. If a story has an image, add it as an enclosure with an absolute address, a type and a length; the asset address that the API returns is already absolute. Text arrives as Unicode and the response declares UTF-8, but control characters that XML forbids can still come from pasted content. See why special characters and emoji break feeds.
People and tools should not have to guess where the feed is. Add a link element to the head of your site layout, with the relation set to alternate, the type set to the RSS media type and the absolute address of the feed. In a React framework, this goes into the metadata of the root layout. A person or a tool that only knows your home page can then find the feed on its own. The full explanation is in the guide to feed autodiscovery.
Finally, think about who edits. Editors are not developers, and they should not need to know that a feed exists. Give the summary field a short help text that says where the text will appear, such as in search results and on social networks, and set a maximum length. A clear field with a clear purpose produces better summaries than any amount of cleaning in code, and it keeps your feed useful as the team changes.
Request the route locally, then test the deployed address from outside, because a local test says nothing about the hosting platform and the certificate in front of your application.
curl -i http://localhost:3000/feed.xml
curl -i https://example.com/feed.xml
curl -i -H 'If-None-Match: "paste-the-etag-here"' https://example.com/feed.xmlA longer routine is in the feed validation guide, and a shorter one in debugging a feed with curl.
PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new entry, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.
A Storyblok feed is a small handler written in your front end, and it has to get the details right: the public token and the published version, the first publication date, a required summary field, stable slugs that match your page addresses, escaped text, absolute links, the correct content type and a validator that lets clients skip unchanged files. Refresh it with a webhook, test the live address with curl and a validator, then connect it to an auto-poster. After that, every story your editors publish reaches your readers and your social channels without extra work.
No. Storyblok delivers content as JSON through its API, so you build the feed in your own front end from the stories that the API returns.
The first publication date. The modification date changes with every edit and would push old stories to the top of the feed.
The public access token with the published version. The preview token can return drafts and must never be used for a public feed.
Set up a webhook for publish events that clears the cached feed or triggers a rebuild, and keep a short cache lifetime as a safety net.
Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new stories are shared as soon as they appear.
What changed in the networks, what broke, and how to fix it before it costs you reach.
Izstrādājusi Internet Solutions — PostRSS komanda. Katrs produkts ietaupa laiku savā veidā.