Svelte RSS feed

Some interesting-enough things I ran into while adding RSS to this site.

Serving XML with Svelte

The site was already set up to generate these post pages from Markdown files, which have YAML frontmatters with some metadata about the post. Since we have the metadata available already, I wanted to generate the rss.xml file automatically instead of updating it myself. After transforming the post data into the RSS format, I used some random library to generate the XML. The part that took the longest was probably the dates, which I'll go into more later (just for fun).

Generating the feed itself was pretty easy, but getting Svelte to serve it properly was a little more complicated. It couldn't be in a normal page since it would get wrapped with the default HTML layout and wouldn't work. Instead, we can create an API route at /rss.xml by creating the file /routes/rss.xml/+server.ts. We'll have this file handle GET requests and respond with the XML.

1
import type { RequestHandler } from './$types';
2

3
export const GET = (() => {
4
    // ... XML generation stuff ...
5
    const body = '...';
6

7
    return new Response(body, {
8
        headers: {
9
            'Content-Type': 'text/xml',
10
        },
11
    });
12
}) satisfies RequestHandler;

The next problem was that links to /rss.xml would be routed by SvelteKit and wouldn't find the API route and would 404 instead. To fix this, SvelteKit provides the attribute data-sveltekit-reload which lets the browser handle the link instead.

1
<a href="/rss.xml" data-sveltekit-reload>
2
    <img src="/rss.svg" />
3
</a>

The last issue was that SvelteKit's adapter-static was complaining about being unable to statically render all routes when it tried to build the site. We just need to add one line to +server.ts:

1
export const prerender = true;

RFC 822 dates without dependencies

The RSS spec requires dates to be in a format described in RFC 822. There's lots of Javascript libraries we could reach for (and since we statically render the site, it wouldn't affect our final bundle size), but let's try and avoid them.

We could use Date.toUTCString(), which the spec says is based on the format in RFC 7231 and appears to be the same as RFC 822. Instead, we can also use Intl.DateTimeFormat, which needs a little more manipulation but handles timezones nicely. We'll use formatToParts and then pick out the parts we need and reformat them into the RFC 822 format:

1
const formatDate = (date: Date) => {
2
    const formatter = new Intl.DateTimeFormat('en-US', {
3
        year: 'numeric',
4
        month: 'short',
5
        weekday: 'short',
6
        day: '2-digit',
7
        hour: '2-digit',
8
        minute: '2-digit',
9
        second: '2-digit',
10
        hourCycle: 'h23',
11
        timeZone: 'America/Los_Angeles',
12
        timeZoneName: 'short',
13
    });
14

15
    const parts = formatter.formatToParts(date);
16

17
    const weekday = parts.find((p) => p.type === 'weekday')?.value;
18
    const day = parts.find((p) => p.type === 'day')?.value;
19
    const month = parts.find((p) => p.type === 'month')?.value;
20
    const year = parts.find((p) => p.type === 'year')?.value;
21
    const hour = parts.find((p) => p.type === 'hour')?.value;
22
    const minute = parts.find((p) => p.type === 'minute')?.value;
23
    const second = parts.find((p) => p.type === 'second')?.value;
24
    const timeZoneName = parts.find((p) => p.type === 'timeZoneName')?.value;
25

26
    return `${weekday}, ${day} ${month} ${year} ${hour}:${minute}:${second} ${timeZoneName}`;
27
};

I definitely didn't figure out the Intl.DateTimeFormat solution first and then discover toUTCString... Thanks for reading.

kckckc © 2023