RSS, Astro, And Me – Part 2

As I mentioned in Part 1 of this installment, while trying to modify my site RSS feed to contain the full body of each post in my feed items, I ran into an inconvenient truth about how MDX exposes its file content as a component and how I could not use that component outside of an Astro component. JavaScript just doesn’t know what it is. Support for the MDX Content component has to be built into whatever framework you’re using.

Astro, of course, is built to use MDX and take full advantage of the MDX Content component, so Astro Discord member Chris Adiante proposed I simply use an Astro component to create the RSS (with full access to the MDX content) and then have it write the rss file to the file system. Since my site is Astro SSG (fully static, only changing when I rebuild the site) and not Astro SSR (server-rendered on demand), I can use this technique without any problems.1

By the way, Chris is the creator of the really amazing looking Astro M²DX remark plugins. If you’re using MDX with Astro, you should definitely give these a look!

The Strategy

In order to write an RSS feed using an Astro component, the Astro component has to get called – or to put it another way, it has to be used in a page. It can’t just sit in a folder in src. Also, it should be called once and once only. It can’t be put inside a layout file used by multiple pages because I want it written just once during the build. And finally, it has to have no effect on the rendering of the page that it’s in. Its purpose is to generate the RSS for all site posts and then write that RSS to a file called rss.xml. It has nothing that should be displayed, and it cannot alter the output of the page that hosts it.

To meet these requirements, I decided to use this Astro component in index.astro, the Astro page that gets built into index.html.

The Components

I use two components to create my RSS file, in the manner suggested by Chris Adiante: WriteFile.astro and RssXml.astro. RssXml.astro generates the RSS and WriteFile takes its output and dumps it to disk in the form a file called rss.xml.


The way RssXml.astro works is dictated by the fact that in order to render, it needs to directly output some html tags outside a javascript loop. This is because Astro components write html. It’s how they work.

The fact that Astro components write html should not be overlooked because it can also mess with the actual RSS XML generated, a truth that caused me much grief until I learned about Fragments and set:html. Using them in the combination <Fragment set:html="" /> outputs RSS XML that isn’t messed with by Astro trying to ruin the format of XML link elements, for example.


import config from "config";
import path from 'path';
import { rfc2822 } from "../components/utilities/DateFormat.js";

const { allPosts } = Astro.props;
const rssHeaderXml = `<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss/styles.xsl" type="text/xsl"?>
<rss version="2.0">
    <description><![CDATA[ ${config.get("description")} ]]></description>

const rssFooterXml = `  </channel>
<Fragment set:html={rssHeaderXml} />
{ =>
  <Fragment set:html={`
      <link>${new URL(
        path.join("/", path.basename(post.file, path.extname(post.file))),
      <guid>${new URL(
        path.join("/", path.basename(post.file, path.extname(post.file))),
      <description><![CDATA[ ${post.frontmatter.description}]]></description>
      <content:encoded><![CDATA[`} />
    <Fragment set:html={`]]></content:encoded>
    </item>`} />)}
<Fragment set:html={rssFooterXml} />

This is not a complex component. All it has to do is generate and output XML. It doesn’t even have to get the posts to include, because those are passed in as a prop.

Some points of note:

  • I create a constant for the top of the RSS file above the items which holds all the channel tags, and a constant for the end of the file after the items (which is just the closing channel and RSS tags).
  • Secondly, the items are created in a JavaScript .map function which takes the array of posts and maps each item to create HTML fragments for them. It’s all very straightforward if you’ve looked at an RSS feed before.
  • The most interesting detail to note by far, and indeed the whole reason behind this custom RSS approach, is the <post.Content/> component inside the .map function. For each post being mapped, I have a <Fragment/> wrapping everything before the post.Content object, then I end it, reference the post.Content object, and then create another Fragment object to wrap up the item XML.

It’s important to understand that <post.Content/> is being accessed directly inside the Rss.Xml Astro component, it’s not inside a JavaScript string or any other wrapper.

It has to be directly written in Astro as a direct Astro component item, or it will be meaningless. It’s Astro that understands what to do with this Content component, not JavaScript or any other framework.


If you’re hoping for a giant code listing that requires lots of explanation, WriteFile.astro will make you sad. It’s ridiculously simple:


import fs from "node:fs/promises";

export interface Props {
  fileUrl: URL;
const { fileUrl } = Astro.props;

if (Astro.slots.has("rss-writer")) {
  const html = await Astro.slots.render("rss-writer");
  await fs.writeFile(fileUrl, html);

It’s 100% frontmatter. There is no output. If this has you thinking “but you just told me you have to have output in an Astro component”, that’s only true for Astro components that have to output any text. This component does not have to, it takes the text generated by RssXml.astro and writes it to disk. I understand completely if that explanation doesn’t clarify things much, but if you start playing with Astro components or pages, you’ll find out what happens when you don’t write html tags or include other Astro components in the base output.2

The important point with this one is waiting for that slot to render before performing fs.writeFile. I never would have known to do this, or especially how to do this. This was all Chris Adiante. The reason it’s necessary to do this is because the page needs to be written up to that point for the RssXml.astro component to do its thing and have something for WriteFile.astro to actually write to disk.

The one thing I did differently to his suggestion is to not look and wait for default slot. Using default slot meant everything before RssXml.astro’s output also got written into the file. Not what I want. As a result, I created a named slot in my Base.astro layout template (which index.astro uses) and then target both WriteFile.astro and RssXml.astro to this slot inside of index.astro.

index.astro content section

This is just the content section of index.astro without any frontmatter, just to show you how I incorporated my WriteFile and RssXml Astro components so that they do their thing when the index page is built.


<Base title={title} , description={description}>
  <section aria-label="Blog post list">
    { => {
        return (
          <Post content={mdxpost}>
            <mdxpost.Content />

    <nav id="pager">
        allPosts.length > pageSize ? (
            <a href="/2">Older Posts</a>
        ) : null

  <WriteFile fileUrl={rssFileUrl} slot="rss-writer">
    <RssXml allPosts={allPosts} slot="rss-writer" />

The above is everything in index.astro except the frontmatter section. The part that writes the RSS file is at the bottom. One thing about named slots that tripped me up is that both components have to name the slot to use explicitly, not just the outer component. Anything you want to wind up in a named slot needs to name that slot.


The final piece of the puzzle is just the named slot at the bottom of Base.astro, my layout template used by index.astro and all my other pages.


import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import "../styles/sw2.css";

export interface Props {
  title: string;
  description: string;
  url: string;

const { title, description } = Astro.props;

    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="title" content={title} />
    <meta name="description" content={description} />
    <link rel="icon" type="image/x-icon" href="/favicon.ico" />
    <Header />
      <slot />
    <Footer />
    <script is:inline src="/scripts/barefoot.min.js"></script>
    <script is:inline>
      lf = new BareFoot();
  <slot name="rss-writer" />


See that innocent looking slot down at almost the very bottom named “rss-writer”? That’s the named slot that allows index.astro to render the WriteFile and RssXml astro components. You might also notice the default <slot/>in between the html <main> tags. That’s where all the content generated by any page that uses Base.astro as its layout gets rendered.

TLDR; and Summary

There’s a lot to digest here. The key takeaways are:

  • Accessing MDX content is done through a Content component, which Astro understands, but JavaScript does not.
  • As a result, trying to access that content for a full-item-body RSS feed requires using Astro components to generate the feed.
  • Astro components only write HTML, not XML or an RSS feed file. Using Astro components to generate XML therefore means writing that to disk yourself.
  • This only works in SSG environments. You would not want to use this with an SSR site because it would write rss.xml over and over again. If I were going to use this on an SSR site, I’d want a page with Astro components that isn’t public facing, which would get called by a node script executed by a cron job or whenever a post is added to the site.

As always, I’m not a brilliant programmer and I’m sure there are better ways of doing everything I did. I also would never have figured out the syntax and layout requirements for this without the help of Chris in the Astro Discord.


  1. Astro currently forces a choice between a fully static site (SSG) or a server-rendered site (SSR). Future support for SSG with SSR routes as needed is on the roadmap (I think).

  2. Hint: Nothing. Nothing is what happens.

RSS, Astro, And Me – Part 1

The first Astro site I put on the web was Siracusa Says, which went live on August 7th. The second Astro site was this site, on August 21st. If you think about how bare bones this site is, and that there’s a 3 week gap there, you might be tempted to think that Astro doesn’t allow for particularly quick development. The truth is, it does, but I also have a day job that was more demanding than normal during that time. In fact, this site was super simple to build. The thing that took me the longest was figuring out a look that I would only have to be 90% ashamed of.

I’m not a designer.

But it’s not all unicorns and fluffy kitties with Astro. Astro is a very new framework and it’s very much a work in progress. One of the late design decisions taken by the development team before Astro 1.0 was released was to stop developing customized markdown with component support, and make markdown just markdown, and use MDX for markdown with component support.

MDX is an interesting animal. If you create an MDX file, the MDX spec will give you access to parts of that file in different ways. For example, the body of the document (in other words, the actual content) is exposed as a component. It’s an object. And being able to access that object depends on whatever framework you’re using supporting MDX and providing that access for you.

Astro does provide this ability to access MDX content as an object. Let’s say you grab all your site’s posts, which happen to be mdx files, using Astro.glob, like this:

let allPosts = await Astro.glob("../content/*.mdx");
allPosts = allPosts.sort(
  (a, b) =>
    new Date( -
    new Date(

You now have an array of posts in the variable allPosts. You can use JavaScript’s map function to deal with them individually, like this:

{ => {
    return (
      <Post content={mdxpost}>
        <mdxpost.Content />

Without worrying too much about the rest of the details here, just understand that <mdxpost.Content /> is a component object that exposes the content from one post. The map contains all the posts, and each mapped mdxpost has a .Content component that holds the content.

If it makes your head hurt and you find it weird, you’re not alone. I guess people coming from JavaScript frameworks like React might be used to things like this - I’m not really sure because I don’t know anything about React. At any rate, this type of JavaScript component is not an unknown thing, it’s just new to me.

Now that you have some of the backstory on how MDX works, let me just say that it created a bit of a problem for me with respect to my RSS feed. The reason for this is that Astro components output HTML. Only HMTL. They can’t output JSON or XML or anything other than HTML. This means the @astrojs/rss package that provides RSS support to Astro doesn’t use Astro components to create the RSS file, it uses JavaScript (most likely TypeScript). It therefore does not support the MDX file Content component object, and it therefore means that creating an RSS feed that Astro way limits me to a summary type feed, without full body content for each item in the feed.

Here’s my rss.xml.js for the Siracusa Says RSS feed as an example:

export const get = () =>
    stylesheet: "/rss/styles.xsl",
    title: config.get("title"),
    description: config.get("description"),
    site: config.get("url"),
    items: Array.from(episodes)
      .map((episode) => ({
        title: episode.frontmatter.title,
        link: new URL(
          path.join(config.get("episodes.path"), episode.frontmatter.slug),
        pubDate: rfc2822(episode.frontmatter.pubDate),
        description: episode.frontmatter.description,
        customData: `<enclosure url="${config.get("episodes.audioPrefix")}/${
        }" length="${episode.frontmatter.bytes}" type="audio/mpeg" />`,

Initially I thought I could use the customData property of the rss package to stuff my MDX file content into, but there is literally no way to get add the .Content component in a way that this JavaScript understands. The best I can do is see the function that returns it or see [object Object]. Not very useful.

To summarize all the above: using MDX as my post content files and @astrojs/rss to support rss feed creation in Astro resulted in my only being able to provide truncated RSS feed items. In order to solve this, I would have to take the advice of Astro Discord member Chris-Adiante and use an Astro component to render the RSS, allowing access to each posts .Content component, and then writing the rss to the filesystem as an rss.xml file.

That’s exactly what I did. I’ll show you how in Part 2.


The last time I posted to my site was on February 24th. That’s 6 months between posts.

Far from being dead, the site was just undergoing a metamorphosis that was invisible to you. And to me, very honestly. I’ve been working on other projects, like Friends with Beer and Siracusa Says, so this site was pushed to the back of the queue for its inevitable and much needed refactoring.

When I built this site, I was new to modern static site generators. I used Hugo, which was super popular at the time (and maybe still is) because of its performance and because the web store was one of two wildly divergent camps: single-page web applications, built in React or similar, and completely static sites with little to no interactivity. Hugo is great for fully static websites because it’s FAST and because it’s a very well thought out framework with many of the features people want included by default. But it uses the horrific Go templating language which was apparently created by Yoda and which broke my brain every time I tried to make an even semi-complicated query or logic statement.

Even worse than Go templating language when I built this site with Hugo was my understanding of how to structure it. I didn’t fully grasp how Hugo built pages and directories based on template names and types, and so I came up with a very convoluted scheme in which all of my different categories had their own folders. Not great. And I should never have had so many categories to begin with, which was another failure of my imagination. The end result was a site that was way too complicated for a guy who just wanted to babble about things most people will never read.

I thought I’d build the next iteration with Eleventy. It’s what I used for Friends with Beer, and I learned a lot from that. The beauty of Eleventy is that it’s Node.js based and allows for templates and layouts that use good old Javascript instead of terrible templating languages, although I primarily used Nunjucks for the templating, because that’s what a lot of the best examples I could find at the time used. Also, since you can just import modules that you or other people write, I was able to just write a lot of straight code in modules and use those in my layouts.

But then as I was getting my head wrapped around THAT mode of working, along came a new framework: Astro. On the surface, Astro is a lot like Eleventy. It’s Node.js based, it allows for a lot of Javascript (actually its default is Typescript) modules, and it has a great static site story. But it’s more than just that, and there are a list of things that make it more attactive to me than Eleventy:

  • It doesn’t use templating languages. It used HTML and Javascript and Components.
  • It was built to allow for flexibility in terms of interactivity. Zero Javascript to the client by default, but an Islands Architecture approach allows adding what you need as you need it. An example is my search page, powered a Solidjs component that executes everything on the client in Javascript.
  • It has an SSR story as well, and you can deploy it to several different platforms including hosting your own with a Node adapter.
  • Finally, it’s just very easy to build with. It’s more reminiscient of the old days of Asp.NET or PHP but with modern frameworks and platform functionality. It’s user friendly for both programmers and site visitors, and it remembers what the web actually is and works with those strengths instead of pretending the web is something it’s not.

Astro had a very tumultous beta period, but the developers were purposefully working towards getting a stable 1.0 that could be used and built upon, and I think they’ve succeeded. There are some weirdnesses and omissions that most of us who’ve used other frameworks notice immediately, but mostly there aren’t, and mostly it’s just very easy and fun to work with. Just as importantly, the team and community behind it is overwhelmingly positive.

This site runs on Astro. It makes use of things like @astrojs/image, @astrojs/mdx, @astrojs/rss, astro-icon, SolidJs, and a few other tools, as well as the standard use of layouts and components that are the backbone of building an Astro site.

I will absolutely write about Astro and some of the things I’ve had to learn on this site. As always, I’m not a professional programmer and I’m certainly not the quickest or smartest. I do it because I enjoy it, and I think that’s ok. More people should feel good about doing things they enjoy but probably wouldn’t be considered authorities on. Life will be better if we get back to allowing each other that more often again.

There’s also the matter of my ever-evolving computer platform approach, and I have made some decisions in that regard lately. I can’t wait to get started on them, but that will take some money I don’t have just yet, and it will be something I write about quite a lot.

For now… let’s begin anew, friend.