Sergio Xalambrí

Build your own RSS Reader

RSS is a standard way to use XML to share content from a website so other apps could read it and show it in a different format.

Years ago, Google Reader was the best tool in the market to consume RSS. When Google killed it to promote Google+ (which was also killed) other tools started to compete but most of them are not as good Google Reader was or they are pay ones.

I used Feedly since 2005, until now, I build my own RSS Reader and you can see it clicking Reader in the navigation. Let's see how you can build one yourself and host it for free.

Stack

The stack I used is the same of this blog, Airtable, Next.js and Vercel.

Airtable, the database

First of all, we need a way to store our list of RSS feed, this could be a JSON in the repository or any database, I used Airtable. A simple table with three columns

Next.js, the framework

We need to fetch our list of feeds, I have a function getRSSFeeds like this one:

import Airtable from "airtable";
import { RSSFeed } from "types";

const base = new Airtable({
  apiKey: process.env.AIRTABLE_API_KEY,
}).base(process.env.AIRTABLE_BASE);

export async function getRSSFeeds(limit = 100): Promise<RSSFeed[]> {
  const table = base("feeds") as Airtable.Table<RSSFeed>;

  const records = await table.select({ maxRecords: limit }).firstPage();

  return records.map((record) => record.fields);
}

The RSSFeed type has this shape:

type RSSFeed = {
  id: number;
  url: string;
  name: string;
};

In the pages/reader/index.tsx file I have a super short getStaticProps function.

import { GetStaticProps } from "next";
import { ReaderProps } from "types";
import { ReaderLayout } from "layouts/reader";
import { getRSSFeeds } from "utils/get-rss-feeds";

export const getStaticProps: GetStaticProps<ReaderProps> = async () => {
  const feeds = await getRSSFeeds();
  return { props: { feeds }, revalidate: 1 };
};

export default ReaderLayout;

The ReaderProps type has this shape:

type ReaderProps = { feeds: RSSFeed[] };

As we can see it's basically two lines of code. Note how revalidate is defined, this enables the incremental regeneration behavior of Next.js for this page, with this if a new feed is added to the Airtable database there is no need to deploy, the page will update itself in background the next time someone access it.

You may notice our ReaderLayout we export. This has the UI, you can do whatever you can, if you like the one I built for myself check it on GitHub.


Now that we have our list of feeds we can create a page for each individual feed, we can create one in pages/reader/[feed].tsx, here we need to define a getStaticProps and a getStaticPaths methods.

import { GetStaticProps, GetStaticPaths } from "next";
import Parser from "rss-parser";
import { FeedPageProps, FeedPageQuery, Feed } from "types";
import { FeedLayout } from "layouts/feed";
import { getRSSFeeds } from "utils/get-rss-feeds";

export const getStaticProps: GetStaticProps<
  FeedPageProps,
  FeedPageQuery
> = async ({ params }) => {
  const id = Number(params.feed);
  const parser = new Parser();
  const feeds = await getRSSFeeds();
  const { url, name } = feeds.find((feed) => feed.id === id);
  const res = await fetch(url);
  const rss = await res.text();
  const feed = ((await parser.parseString(rss)) as unknown) as Feed;
  return { props: { feed, id, name, url }, revalidate: 1 };
};

export const getStaticPaths: GetStaticPaths<FeedPageQuery> = async () => {
  const feeds = await getRSSFeeds();

  const paths = feeds.map((feed) => {
    return { params: { feed: feed.id.toString() } };
  });

  return { paths, fallback: "blocking" };
};

export default FeedLayout;

Here we need to import the rss-parser lib which will let us read a string with RSS in XML and return an object with the data from the feed. The code for this is in the getStaticProps function.

In the getStaticPaths we call getRSSFeeds and transform the list of feeds from the Airtable DB to the list of paths. We also enable fallback as blocking which means if a user access a feed we didn't considered initially in our list of paths we will do SSR and then cache the HTML and continue with SSG.

The FeedPageProps, FeedPageQuery and Feed types have this shapes:

type FeedPageProps = {
  id: RSSFeed["id"];
  url: RSSFeed["url"];
  name: RSSFeed["name"];
  feed: Feed;
};

type FeedPageQuery = { feed: string };

type Feed = {
  title: string;
  description: string;
  lastBuildDate: string;
  items: Array<{
    guid: string;
    title: string;
    link: string;
    content: string;
    contentSnippet: string;
    isoDate: string;
    pubDate: string;
  }>;
};

And as with the ReaderLayout, the exported FeedLayout is the UI of each individal feed. You can see the code for the one in my blog on GitHub too.

Vercel, the hosting platform

Lastly, we can deploy our site to Vercel, go to Vercel project importer import your GitHub repository.

Vercel will give you free hosting with a small limit of server functions, thank to using getStaticProps for everything our RSS reader will not be counted and we can have any number of feeds.