Contents

How to generate an RSS feed with a headless CMS

Some of you already asked to receive a tutorial on this topic. So we will demonstrate how to use the Storyblok APIs to create an RSS feed according to content-entries, to be specific only news content types (also works with other content entries). We will use JavaScript to load the content and display it on a website to get you the idea on how to do that.

Goal

We want to access to Stories API to generate an RSS feed for our project according to our content entries (news) in Storyblok. To do so we’ll use the content after our blog tutorials, links to those will be at the end of the article. The basic structure is to have post, author and category content types and combine those into our RSS feed.

Image of folder structure

Let’s start

We will use the Stories API of Storyblok that allows us to load our content entries including the content. If you only want to generate a Sitemap where you would not need the content - check out our tutorial on how to create a sitemap. You can find a documentation for the Stories API and all its parameters linked right here.

To get you started we will do a simple GET request to load a list of stories. The structure of the response is an array that contains Story objects and the content for each of them. Since we only need the articles we can apply a query param to load those content entries in the posts folder.

// view JSfiddle: https://jsfiddle.net/DominikAngerer/xfu7edz2/5/
let preview_token = 'ask9soUkv02QqbZgmZdeDAtt'

axios.get(`https://api.storyblok.com/v1/cdn/stories?starts_with=posts&token=${preview_token}`)
.then((result) => {
  document.querySelector('#storiesresponse').innerHTML = JSON.stringify(result.data, null, 2)
})

Load all content entries

The result you can see above would, by default, be a collection with 25 entries. Each of those entries is one Story (a content entry). By using the starts_with parameter we filter all content entries that their slug will have to start with posts. To be able to load more than 25 we can either add the per_page=1000 parameter or (our recommendation) load it in a paged manner. Doing it like that you will never have to update the per_page parameter if you will have more than 1000 content-entries since 1000 would be the max size of the per_page param. You can check out the example code below for this using Axios. We will use the Total header of the response to generate all requests we need to perform and execute them to load our content entries. You can do the same with every other technology in the same way by access that header.

// view JSfiddle: https://jsfiddle.net/DominikAngerer/25d7zgjx/9/
let preview_token = 'ask9soUkv02QqbZgmZdeDAtt'
let per_page = 1 // since we only have 3 entries to demonstrate the paged behavior I will use 1 - I would recommend a value of 100-1000 or stick with the the default 25.
let page = 1
let all_stories = []

// Call first Page of the Stories API: https://www.storyblok.com/docs/Delivery-Api/get-a-story
axios.get(`https://api.storyblok.com/v1/cdn/stories?starts_with=posts/&token=${preview_token}&per_page=${per_page}&page=${page}`).then((res) => {
  // push all stories into our all_stories variable
  res.data.stories.forEach((entry) => {
    all_stories.push(entry)
  })

  // Check if there are more pages available otherwise thats all to do.
  const total = res.headers.total
  const maxPage = Math.ceil(total / per_page)
  if (maxPage <= 1) {
    // since we only have one page we can output it directly
    document.querySelector('#storiesresponse').innerHTML = JSON.stringify(all_stories, null, 2)
    return
  }

  // Since we know the total we now can generate all requests we need to get all entries
  let contentRequests = []

  // we will start with page two since the first one is already done.
  for (let page = 2; page <= maxPage; page++) {
    contentRequests.push(axios.get(`https://api.storyblok.com/v1/cdn/stories?starts_with=posts/&token=${preview_token}&per_page=${per_page}&page=${page}`))
  }

  // Axios allows us to exectue all requests using axios.spread. We will then push our stories into our all_stories variable.
  axios.all(contentRequests).then(axios.spread((...responses) => {
    responses.forEach((response) => {
      response.data.stories.forEach((entry) => {
        all_stories.push(entry)
      })
    })

    // now we have all our stories available in the all_stories variable
    document.querySelector('#storiesresponse').innerHTML = JSON.stringify(all_stories, null, 2)
  }))
})

Build the RSS feed

The final thing to do is to use those news entries to generate an RSS feed. Since a basic RSS feed will have items that look like those below. As you can see it’s basically an XML that follows the RSS specification. You can read more about the RSS specification on the harward.edu page.

<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
 <title>RSS Title</title>
 <description>This is an example of an RSS feed</description>
 <link>http://www.example.com/main.html</link>
 <lastBuildDate>Mon, 06 Sep 2010 00:01:00 +0000 </lastBuildDate>
 <pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
 <ttl>1800</ttl>

 <item>
  <title>Example entry</title>
  <description>Here is some text containing an interesting description.</description>
  <link>http://www.example.com/blog/post/1</link>
  <guid isPermaLink="false">7bd204c6-1655-4c27-aeee-53f933c5395f</guid>
  <pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
 </item>

</channel>
</rss>

In the next code example, we’ll output it with escaped HTML entities so you can see the result directly. However, you should make sure to output it as a proper XML.

// view JSfiddle: https://jsfiddle.net/DominikAngerer/79mwnjt3/29/
let preview_token = 'ask9soUkv02QqbZgmZdeDAtt'
let per_page = 1
let page = 1
let all_stories = []

// Call first Page of the Stories API: https://www.storyblok.com/docs/Delivery-Api/get-a-story
axios.get(`https://api.storyblok.com/v1/cdn/stories?starts_with=posts/&token=${preview_token}&per_page=${per_page}&page=${page}&cv={new Date()}`).then((res) => {
  // push all stories into our all_stories variable
  res.data.stories.forEach((entry) => {
    all_stories.push(entry)
  })

  // Check if there are more pages available otherwise thats all to do.
  const total = res.headers.total
  const maxPage = Math.ceil(total / per_page)
  if (maxPage <= 1) {
    return
  }

  // Since we know the total we now can generate all requests we need to get all entries
  let contentRequests = []

  // we will start with page two since the first one is already done.
  for (let page = 2; page <= maxPage; page++) {
    contentRequests.push(axios.get(`https://api.storyblok.com/v1/cdn/stories?starts_with=posts/&token=${preview_token}&per_page=${per_page}&page=${page}&cv={new Date()`))
  }

  // Axios allows us to exectue all requests using axios.spread. We will then push our stories into our all_stories variable.
  axios.all(contentRequests).then(axios.spread((...responses) => {
    responses.forEach((response) => {
      response.data.stories.forEach((entry) => {
        all_stories.push(entry)
      })
    })
    
    let rss_entries = all_stories.map((story) => {
      // you got access to every property of the stories here. Note the \n I've added to format it in the output - you don't need that in the real XML.
      // our post content type uses one property "content", if your content type is in a different structure - at this point you can prepare your entries as you need it to fit the format of an RSS feed.
      return `\n<item>
  <title>${story.name}</title>
  <description>${story.content.description}</description>
  <link>http://www.example.com/${story.full_slug}</link>
  <guid isPermaLink="false">${story.uuid}</guid>
  <pubDate>${story.published_at}</pubDate>
 </item>`
    })
    
  // the actual rss wrapper with all it's entries.
  let rss = `<?xml version="1.0" encoding="UTF-8" ?>
  <rss version="2.0">
  <channel>
   <title>RSS Title</title>
   <description>This is an example of an RSS feed</description>
   <link>http://www.example.com/</link>
   <ttl>1800</ttl>
        
   ${rss_entries.join('')}

  </channel>
  </rss>`

    // now we have all our stories available in the all_stories variable
    document.querySelector('#storiesresponse').innerHTML = rss.replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&apos;');
  }))
})

Summary

You’ve now learned how to access the stories API and use it’s content to generate an RSS feed. You can use the same approach to generate your own RSS feed with another technology like PHP, Python or any other. A similar setup is also used to build a sitemap, instead of the stories API, it will use the links API tho since you won’t need the entries content for a sitemap. RSS feeds are capable of using the Authors and Categories tags if you want to add them to your RSS feed you can check out the tutorials in the table below to learn how to resolve the relationships between content entries.

ResourceLink
How to create a blog content entryhttps://www.storyblok.com/tp/how-to-create-a-blog-content-structure
How to create a post <> author relationshiphttps://www.storyblok.com/tp/how-to-build-relationships-between-2-content-types
How to create a post <> categories relationshiphttps://www.storyblok.com/tp/how-to-build-a-content-relationship
How to generate a sitemap with a headless CMShttps://www.storyblok.com/tp/how-to-generate-sitemap-headless-cms

More to read...

About the author

Dominik Angerer

Dominik Angerer

A web performance specialist and perfectionist. After working for big agencies as a full stack developer he founded Storyblok. He is also an active contributor to the open source community and one of the organizers of Stahlstadt.js.