Let Your Builder Build Your RSS Feed
I have a fundamental belief that you should ruthlessly automate drudgery. So when I decided to add an RSS feed to this blog, my first thought was, "How can I make this require zero ongoing effort?"
The web is full of tutorials that would have you spin up a full-blown server or subscribe to a SaaS platform to handle your RSS feed. This felt absurd for a site that is, by its very nature, static. The content only changes when I build and deploy. Why should the feed be any different?
It shouldn’t. The feed is an artifact of the build, just like a CSS bundle or a minified image. And if it's a build artifact, the builder should build it.
First, a Quick Refresher: What is RSS?
Before we dive in, let's freshen up what RSS even is. Standing for "Really Simple Syndication," RSS is a standardized XML file format that allows users to subscribe to content from a website. Think of it as a direct, chronological feed of your latest posts, free from algorithms or platform-specific whims. A user points an "aggregator" or "feed reader" app at your rss.xml file, and the app does the rest, pulling in new content as you publish it. It's a simple, robust, and open way to own your relationship with your readers.
The Right Tool for the Job: Vike's Build Hooks
This site is built with Vike.js, a framework that sits on top of Vite. While Vite handles asset bundling, Vike provides the structure for server-side rendering (SSR) and static-site generation (SSG). It's what lets me build a React app that ultimately compiles down to plain HTML files.
Fortunately, modern tools like Vike are designed for this kind of task. Vike provides an onPrerenderStart hook, which is just a fancy name for "a function I'll run for you once before I build all your pages." It's the perfect place to slot in our RSS generation logic.
The plan became clear and simple:
- At the start of the build, find every blog post.
- For each post, grab the metadata I've already written (title, description, date).
- Use a simple library like
rssto format this data into an XML string. - Save that string as
rss.xmlin the final output directory.
The result is a process that is fully automated, requires no runtime server, and adds practically nothing to my build time. It’s the kind of zero-bloat, "just works" solution I strive for.
Here’s a simplified version of the code that makes it happen, living in renderer/+onPrerenderStart.ts where Vike automatically picks it up:
// renderer/+onPrerenderStart.ts
import fs from 'node:fs';
import path from 'node:path';
import RSS from 'rss';
import type { Post } from '../pages/blog/types';
export const onPrerenderStart = async () => {
const feed = new RSS({
title: "Seva's blog",
description: 'Blog posts about development, design, and other things.',
site_url: 'https://seva.dev',
feed_url: 'https://seva.dev/rss.xml',
language: 'en',
pubDate: new Date(),
copyright: `${new Date().getFullYear()} Seva Maltsev`,
});
// Vite's import.meta.glob is the magic that finds all post configs statically.
// https://vitejs.dev/guide/features.html#glob-import
const postConfigs = import.meta.glob("../pages/blog/*/+config.ts", { eager: true });
const posts = [];
for (const configPath in postConfigs) {
const module = postConfigs[configPath];
if (!module || !module.postData || module.postData.hide) continue;
const slug = configPath.replace("../pages/blog/", "").replace("/+config.ts", "");
// Filter out special pages that aren't real posts
if (['all', 'tags', 'template'].includes(slug)) continue;
const postData = module.postData;
posts.push({
title: postData.title,
description: postData.description,
url: `https://seva.dev/blog/${slug}`,
guid: `https://seva.dev/blog/${slug}`,
date: new Date(postData.date),
author: 'Seva Maltsev',
});
}
// Sort by date and add each post to the feed
posts.sort((a, b) => b.date.getTime() - a.date.getTime())
.forEach(post => feed.item(post));
// Write the final file. Done.
const publicDir = path.resolve(process.cwd(), 'dist/client');
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(path.join(publicDir, 'rss.xml'), feed.xml({ indent: true }));
};
And that's it. By letting the builder do the work, the problem stays solved. The feed is always up to date with the last deployed build, and I never have to think about it again. It's one less piece of drudgery, freeing up time for the work that actually matters.