Important NOTE
This post describes a flow for a blog that sources posts from a static directory (e.g. Github repo of markdown files). If your blog content is hosted on a CMS, you will need to modify this solution so that your feed file is updated each time a new post is added to your CMS. The overall strategy should resemble the one below.
Why an RSS feed?
RSS feeds are rarely used anymore, but can be great for app-to-app communication and automations. For example, I use a Github Action on my Github profile's README to generate a list of the last 5 blog posts that I have written on my site:
name: Latest blog post workflow
on:
schedule: # Run workflow automatically
- cron: "0 * * * *" # Runs every hour, on the hour
workflow_dispatch: # Run workflow manually (without waiting for the cron to be called), through the GitHub Actions Workflow page directly
jobs:
update-readme-with-blog:
name: Update this repo's README with latest blog posts
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Pull in site posts
uses: gautamkrishnar/blog-post-workflow@v1
with:
# 👇 Here's where I'm reading the RSS feed from my site
feed_list: "https://www.zachgollwitzer.com/rss.xml"
High level solution overview
An RSS feed is simply an .xml
file hosted somewhere on your site that follows this general format:
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Your feed title</title>
<link>https://yoursite.com</link>
<description>Feed description</description>
<item>
<title>Feed item 1</title>
<link>Feed item 1 URL</link>
<description>Feed item 1 excerpt</description>
</item>
<item>
... more feed items go here ...
</item>
</channel>
</rss>
Our goal is to generate a new version of this every time posts are added to our Next.js blog at the following url (could be any url):
https://yoursite.com/rss.xml
To do this, we will...
- Create a
postbuild
step for our NX "app" that dynamically fetches the last 10 posts (or feed items you want to curate). This runs directly after Next.js runs a build (i.e.next build
) - In
postbuild
script, fetch latest 10 posts and use therss
NPM module to generate an XML file from it. - Write that file to
/public/rss.xml
(where Next.js hosts static assets)
Step 1: Wire up your postbuild step with nx
With Nrwl NX, you can have multiple apps in a single repository. These are stored in the /apps
folder. Each app has separate "targets" that tell NX how to serve your app locally, build it, test it, and pretty much anything else you can think of.
For example, to build an app called /apps/blog
, you'd run:
npx nx build blog
We can also write custom targets. In your workspace.json
file, add the following:
"postbuild": {
"executor": "nx:run-commands",
"options": {
"commands": [
{
"command": "npx ts-node tools/scripts/generateRSSFeed.ts"
}
]
}
}
With this configuration, our target, postbuild
will run a script stored in /tools/scripts/generateRSSFeed.ts
with the command:
# Assumes your app is called "blog"
npx nx postbuild blog
Go ahead and create a file at that path and add some testing code to make sure things work okay.
// File: /tools/scripts/generateRSSFeed.ts
import fs from "fs";
import path from "path";
fs.writeFileSync(
// In production build, assets are written to the /dist folder, so check ENV to know where to write this file
path.join(
process.cwd(),
`${
process.env.NODE_ENV === "production" ? "./dist/" : "./"
}apps/blog/public/rss.xml`
),
"TEST FILE CONTENTS"
);
Now, run npx nx postbuild blog
. This should create the RSS file and you should be able to visit it at yoursite.com/rss.xml
. Once you get this working, onto the next step!
Step 2: Use the RSS module to generate an RSS xml file
First, install the rss
module:
yarn add rss
yarn add -D @types/rss
Now, update your postbuild script (/tools/scripts/generateRSSFeed.ts
):
import fs from "fs";
import path from "path";
import RSS from "rss";
const feed = new RSS({
title: "Zach Gollwitzer Blog RSS Feed",
description: "Latest 10 posts from my blog",
feed_url: "https://www.zachgollwitzer.com/rss.xml",
site_url: "https://www.zachgollwitzer.com",
});
// Just a little TypeScript magic to extract the item option types since they aren't exported from the rss module (not necessary, just helpful)
type RSSItemOptions = Parameters<typeof feed["item"]>[0];
type RSSPost = Pick<
RSSItemOptions,
"title" | "date" | "author" | "description" | "url"
>;
// PLACEHOLDER implementation - will update in next step
async function fetchPosts(): Promise<RSSPost[]> {
return Promise.resolve([
{
title: "Test post",
date: new Date(),
author: "Zach Gollwitzer",
description: "Test description",
url: "www.zachgollwitzer.com",
},
]);
}
// Fetch your posts and write the file
fetchPosts().then((posts) => {
// Add an RSS item for each post
posts.forEach((post) => feed.item(post));
fs.writeFileSync(
path.join(process.cwd(), "./apps/blog/public/rss.xml"),
feed.xml() // Writes the XML file
);
});
Now run npx nx postbuild blog
to test that everything is working okay!
Step 3: Finish your fetchPosts implementation
The final step is to finish your implementation of fetchPosts
, an async function that reaches out to some data source and curates the last X posts (i'm using 10) published to your blog.
Here's an example implementation I have used. My blog posts are stored as markdown files in the /apps/blog/posts
folder, but yours might be different (so adjust accordingly).
I am using the gray-matter
NPM package to extract the metadata about each post. My markdown frontmatter looks like this:
---
title: Title
slug: slug
excerpt: >-
Excerpt
publishedAt: "2022-12-03"
tags: [sample-tag]
category: sample-category
---
import matter from 'gray-matter';
// In my case, I did this synchronously, but if you're dealing with an external CMS, it will be an async function
function fetchPosts(): RSSPost[] {
return (
fs
// Get all valid post filenames
.readdirSync(path.join(process.cwd(), './apps/blog/posts'))
.filter((path) => /\.mdx?$/.test(path))
// For each filename, get the file contents, read the frontmatter, map to the RSSPost type
.map((filename) => {
const filePath = path.join(
process.cwd(),
'./apps/blog/posts',
filename
);
const fileContents = fs.readFileSync(filePath);
const postMeta = matter(fileContents).data;
return {
title: postMeta.title,
date: new Date(postMeta.published_date),
author: 'Zach Gollwitzer',
description: postMeta.excerpt,
url: `https://www.zachgollwitzer.com/posts/${postMeta.slug}`,
};
})
.sort((a, b) => b.date.valueOf() - a.date.valueOf()) // Sort newest => oldest
.slice(0, 10); // Only grab first 10 posts
);
}