How to make an RSS feed in SvelteKit
Chances are, if you’re consuming a lot of content, you’re not checking a ton of individual sites. You may be checking something like Reddit, or another aggregator, or possibly one of the bigger blogging platforms nowadays (dev.to, medium, etc). But that still leaves out large portions of the Internet.
If you control your own website and channel, and you’re using SvelteKit, then you’ll likely want an RSS feed so that your end users can subscribe to your content in their favorite feed reader.
So, what’s it take to to it with SvelteKit? Not a lot!
Note: if you’d rather watch a video tutorial on how to implement an RSS feed, you can check out my YouTube video here.
Here’s the complete code for this blog’s rss feed:
routes/rss.js
export const get = async () => {
const res = await fetch(import.meta.env.VITE_BASE_ENDPOINT + '/posts/posts.json');
const data = await res.json();
const body = render(data.posts);
const headers = {
'Cache-Control': `max-age=0, s-max-age=${600}`,
'Content-Type': 'application/xml',
};
return {
body,
headers,
};
};
const render = (posts) => `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="http://wwww.davidwparker.com/rss" rel="self" type="application/rss+xml" />
<title>David W Parker</title>
<link>https://www.davidwparker.com</link>
<description>David W Parker's blog about Code, Entrepreneurship, and more</description>
${posts
.map(
(post) => `<item>
<guid>https://www.davidwparker.com/posts/${post.slug}</guid>
<title>${post.title}</title>
<link>https://www.davidwparker.com/posts/${post.slug}</link>
<description>${post.description}</description>
<pubDate>${new Date(post.published).toUTCString()}</pubDate>
</item>`
)
.join('')}
</channel>
</rss>
`;
Let’s break it down
The endpoint
// GET /rss
export const get = async () => {
const res = await fetch(import.meta.env.VITE_BASE_ENDPOINT + '/posts/posts.json');
const data = await res.json();
const body = render(data.posts);
const headers = {
'Cache-Control': `max-age=0, s-max-age=${600}`,
'Content-Type': 'application/xml',
};
return {
body,
headers,
};
};
This is a get
request that lives at /rss
. In it, I make a simple request to /posts/posts.json
to get all the blog
articles that I want for this RSS feed.
I call res.json()
to get the resulting json, then send the posts within that json to the render
method to build my body.
Once I get the body, I set a few headers, and return the resulting body and header which is needed for the SvelteKit endpoint.
The body
const render = (posts) => `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="http://wwww.davidwparker.com/rss" rel="self" type="application/rss+xml" />
<title>David W Parker</title>
<link>https://www.davidwparker.com</link>
<description>David W Parker's blog about Code, Entrepreneurship, and more</description>
${posts
.map(
(post) => `<item>
<guid>https://www.davidwparker.com/posts/${post.slug}</guid>
<title>${post.title}</title>
<link>https://www.davidwparker.com/posts/${post.slug}</link>
<description>${post.description}</description>
<pubDate>${new Date(post.published).toUTCString()}</pubDate>
</item>`
)
.join('')}
</channel>
</rss>
`;
We start by making our xml declaration and using the proper rss
tag with the definition from w3.org.
From there, it’s just a standard rss
feed, which you can find from anywhere on the Internet.
In my example, I have a channel
, with atom:link
which references itself. Inside, I have a title for my feed/site,
and a description. From there, I map each of my resulting posts into their own <item>
tag along with their own
guid
, title
, link
, description,
and pubDate
. Close out the tags, and we’re done.
posts.json
This is less important, but it’s just another get
endpoint that returns a bunch of posts from imported md
files.
At this point, there’s a bunch of examples of this all around the Internet- but here’s mine just in case you haven’t seen it yet:
// GET /posts/posts.json
export const get = async ({ query }) => {
let posts = await Promise.all(
Object.entries(import.meta.glob('./*.md')).map(async ([path, page]) => {
const { metadata } = await page();
const slug = path.split('/').pop().split('.').shift();
return { ...metadata, slug };
})
);
if (query.get('q') !== null) {
posts = posts.reduce((accum, val) => {
if (val.categories.includes(query.get('q'))) {
accum.push(val);
}
return accum;
}, []);
}
posts.sort((a, b) => (a.published > b.published ? -1 : 1));
return {
status: 200,
body: { posts },
};
};