2024-08-27

I have to confess: while I ADORE Svelte, I find SvelteKit terribly unintuitive. It probably doesn't help that I only look at it like once a quarter, when I finally have a free day to goof around with this website.

I finally figured out how to publish an RSS feed of my blog posts, and since I had to cobble this together from outdated blogs and Stack Overflow questions, I figured it was worth documenting. Now I will be the outdated blog post!

(This is Svelte ^3.54.0 and SvelteKit ^1.5.0, which, yes, is one major version behind on each. Listen man, I'm not gonna spend all weekend learning new paradigms and migrating breaking changes, I have some self-respect.)

Ok ok. The code. First, make a directory at src/routes/rss.xml. Add a file named +server.js. For some reason, this is how you make an endpoint at mywebsite.com/rss.xml.

//+server.js
import { getPostsAsXML } from "$lib/my-extremely-performant-and-elegant-post-parser";
export const prerender = true; //if you skip this line you'll get 500 errors, but only after you deploy it to an actual server. Don't skip this line.

export const GET = () => {
    const posts = getPostsAsXML();

    const headers = {
      'Content-Type': 'application/xml',
    };

    return new Response(posts, {...headers});   
  };

I'll get to the rest in a sec, but first I should say that my blog posts are written in markdown with a bunch of "front matter" at the top. They look something like this:

---
title: >-
    Add an RSS feed to your SvelteKit website
description: It's 2024 and blogs are more needed than ever.
pub_date: "2024-08-27"
slug: sveltekit-rss-feed
img: jumpscare.jpg
---
I have to confess: while I ADORE Svelte, etc etc etc this is the body of the post

And live at src/lib/blogposts.

I use a package called front-matter to parse the stuff above the fold when converting my markdown into HTML, so I'm re-using it here. Has this package been updated since 2020? Absolutely not. Do I really need it? No, I am perfectly capable of writing a little script that looks at a text file and converts it to an object. And if you're allergic to dependencies, by all means, roll your own. For me, it's 10 pm on a Monday night and I really just want to get back to playing Balatro.

Ok, now we can continue.

//my-extremely-performant-and-elegant-post-parser.js
import fs from 'fs';
import frontMatter from "front-matter";

const path = "src/lib/blogposts";

export function getPostsAsXML() {
    const fileNames = fs.readdirSync(path);
    const posts = fileNames.map((filename) => getAttributes(path + "/" + filename))
        .sort((a,b) => {
            return new Date(a.pub_date) < new Date(b.pub_date) ? 1 : -1}
            );
   return render(posts);
}

function getAttributes (path) {
     const file = fs.readFileSync(path, 'utf8');
     const { attributes } = frontMatter(file);
     return attributes;
 }

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="https://nextlevelbanana.live/rss" rel="self" type="application/rss+xml" />
<title>NextLevelBlognana: the NextLevelBanana Blog</title>
<description>NextLevelBanana Blog</description>
<link>https://nextlevelbanana.live/blog</link>
${posts
    .map(
      ({slug, description, title, pub_date, img}) => {
        const imgElement = img ? `<![CDATA[<img src="/images/${img}">]]>` : "";
        
        return `<item>
  <guid>https://nextlevelbanana.live/blog/${slug}</guid>
  <title>${title}</title>
  <link>https://nextlevelbanana.live/blog/${slug}</link>
  <description>${imgElement}${description}</description>
  <pubDate>${pub_date}</pubDate>
  </item>`
})
    .join('')} 
</channel>
</rss>
`;

The full RSS spec is here. Go ham customizing your feed. Mine includes an optional image with my description; if you don't want to include any images ever, change the description node (do we call them nodes in XML? Elements? Whatever, you get it) to <description>${description}</description>.

The final step is to track down a decent-but-free RSS icon and add a link elsewhere on your site. Did you know that symbol isn't part of unicode? We've got 922 code points of cuneiform, we've got business man levitating emoji, but the universal "feed" symbol is somehow a bridge too far.

Ok ok, actually, there are 2 more steps. The penultimate step is to spam your new feed link across all your socials. Let your followers know that you have ascended to the next plane of existence. Bask in their awe.

And the actual final step, obviously, is to write a blog post about how you did it.