Full content RSS feed
Despite me very rarely writing much on here, I've tried to give myself a good developer experience on my own blog. I write posts in MDX, which is Markdown + JSX. But this presented some challenges with one thing in particular I've wanted to do for a while: I want to provide an RSS feed with the full content of my posts in it! That means if you subscribe to my feed in your RSS reader, you can read posts right there. This has been tricky, but I think I have done it, and here's my path to getting there!
What to Generate
The goal here is to generate an HTML-only version of each post at build time. If my posts were just in Markdown, that would be pretty easy. There are NPM packages that take Markdown and output HTML because there's a straightforward mapping of Markdown syntax -> HTML element. But MDX complicates this: React elements certainly don't have a straightforward mapping to HTML elements! So how can we go from MDX -> HTML at build time?
One place we can look for a hint is within the rest of my website, where this is already happening. My posts are only written in MDX, and the post pages of my site are statically generated at build time according to the output of npm run build
. The component that does this is <MDXRemote />
from next-mdx-remote/rsc
.1 This means that somehow, some code from next-mdx-remote
is going from MDX -> JSX, and then Next.js is taking that JavaScript and generating HTML + JS (or calling the generated JavaScript client side for client components). So all I need to do is follow that same series of steps, right?
In my code to enumerate my posts and generate an RSS feed, I could try using the same <MDXRemote />
component to go from MDX -> JSX, and then call renderToStaticMarkup
from react-dom/server
to generate HTML from that. But I tried this a year ago and it failed (see my issue). But, then I tried again recently and I was able to get it to work! It seems like there's still some weirdness here: for some reason it works2 if I call MDXRemote
as a function, but not a component, and I also have to replace the custom components I pass with built in DOM elements. But it's a start!
When to Generate
Previously I just exported my RSS feed generation code as a function and called it in getStaticParams
, which supposedly only runs at build time, but that felt weirdly tied into Next.js. That code also seemed to run multiple times locally, and I didn't really want that, so I also wanted to try changing the feed generation code to run in a prebuild
npm script.
Honestly, this was tricker than I thought. I got caught in a rabbit hole of using .js(x)
/.ts(x)
/.mjs
files, commonjs
vs ESM imports, dynamic imports, and trying all sorts of incantations in my tsconfig.json
and package.json
. Certain combinations of these things seemed to work okay, but I didn't like how brittle it seemed. Later I tried using tsx
, and it Just Worked™️ with TS and ESM imports, so I've stuck with that!
How to generate
So at this point I had full-content feeds, but I didn't have my custom components, which are kind of the whole point of MDX! If I tried to pass them to the MDXRemote
function (component), I kept getting errors. And of course, it was quite a wild goose chase to resolve them. My first thought was to try to convert them from React components into regular JavaScript files, so of course, I tried using babel with the plugin-transform-react-jsx-development
plugin, which mostly worked. Then I realized I'm already using TypeScript, and therefore tsc
, so I tried doing the operation with npx tsc path/to/file.tsx --jsx react
. But eventually I realized I didn't need to do any of this, and all of my custom MDX components work except for ones that return <Image />
from next/image
. So can use all of my custom components, except for my <Image />
wrapper. Oh well, maybe I'll figure that out down the line!
I'm really happy with this setup, it even means I can include "client only" components in my RSS feed, like this:
Of course, the JavaScript required to make the component interactable won't run in your RSS reader, but it will on the actual website.
Footnotes
-
I know they're being rendered at build time because if I
console.log('AHHHHHHHH')
in the actual component that mounts<MDXRemote />
, I see severalAHHHHHHHH
s when doingnpm run build
. ↩ -
One fun implementation detail: I needed to remove the frontmatter from the start of my MDX files before passing them to
MDXRemote
, so I wrote the world's worst frontmatter parser:source.replace(/---(.|\n)*---/, "")
EDIT: it turns out that the world's worst frontmattter parser is so bad, it broke this post in RSS readers. My regex pattern was greedily removing all text in between two instances of three hyphens, and the second one it found was in this same footnote! I just had to make the pattern non-greedy by adding a question mark after the asterisk:source.replace(/---(.|\n)*?---/, "");
↩