RSS—short for Really Simple Syndication—is an XML-based content syndication format, designed to share content across different platforms. In plain English, RSS is a machine-readable file telling a reader program the list of content entries (posts, podcast episodes, etc.) from a certain site. It’s kind of like the role of “follow” or “subscribe” in modern social media, but in an open and decentralized manner.

People keep saying RSS is dead or is dying. Not really. I mean, yep, RSS/Atom usage is declining, largely thanks to all sorts of social media domination and Google killing Google Reader in the struggle of making their own social media. Still, it’s an oasis in this age of information overload, algorithmic attention grabbing, and deliberate walled-gardenization.

The site publishes a feed. I grab the feed with my reader of choice. No unnecessary third-party involved—no billionaires who have no idea how to run a social media involved.


Naturally, when I started this site, I also wanted it to have a good RSS feed. I was in luck. The tools I used to make this site—Hugo with PaperMod—already had great support of this, without me specifying anything. It worked in my self-hosted FreshRSS instance, and I didn’t know much about RSS then.

That is, until I came across this article titled “RSS Feed Best Practices” by Kevin Cox—it was a way deeper rabbit hole than I imagined. The Hugo and PaperMod setup already implemented many of these suggestions, and Cloudflare Pages took care the caching side of things fine. Still, there are always things we can tinker.

Here are the changes I made to the default setup to follow the best practices listed in the article above.

Atom

I intended to make my site serve Atom feeds, as this format is cleaner and more modern, and practically all RSS readers can understand it.

The RSS template that came with PaperMod is in RSS 2.0. However, the template is easy to adapt to Atom based on the examples given in their RFC document.

Below is the feed template I derived from the original PaperMod template(MIT-licensed):

{{- $pctx := . -}}
{{- if .IsHome -}}{{ $pctx = site }}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $pages = $pctx.RegularPages -}}
{{- else -}}
{{- $pages = $pctx.Pages -}}
{{- end -}}
{{- $limit := site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>{{ if eq  .Title  site.Title }}{{ site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ site.Title }}{{ end }}</title>
  <id>{{ .Permalink }}</id>
  <link rel="alternate" type="text/html" hreflang="en" href="{{ .Permalink }}" />
  {{ with .OutputFormats.Get "RSS" -}}
    <link rel="self" type="{{ .MediaType.Type | html }}" href="{{ .Permalink }}" />
  {{- end }}

  <generator uri="https://gohugo.io/" version="{{ hugo.Version }}">Hugo</generator>
  {{ with site.Author.name -}}
    <author>
      <name>{{.}}</name>
    </author>
  {{- end }}
  {{ with site.Copyright -}}
    {{- $copyright := replace . "YEAR" now.Year -}}
    {{- $copyright = replace $copyright "&copy;" "©" -}}
    <rights>{{ $copyright | plainify }}</rights>
  {{- end }}
  {{ if not .Date.IsZero -}}
    <updated>{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
  {{- end }}

  {{ range $pages }}
    {{- if and (ne .Layout `search`) (ne .Layout `archives`) }}
      <entry>
        <title>{{ .Title }}</title>
        <link href="{{ .Permalink }}?utm_source=atom_feed" rel="alternate" type="text/html" />
        <id>{{ .Permalink }}</id>
        {{ with .Params.author -}}
          <author>
            <name>{{.}}</name>
          </author>
        {{- end }}
        <published>{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</published>
        <updated>{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
        <content type="html">
          {{ printf `<![CDATA[%s]]>` .Content | safeHTML }}
        </content>
      </entry>
    {{- end }}
  {{ end }}
</feed>

Full Content

If you don’t care about the format—you just want to serve the full text in your site feed, there’s an easier way.

By default, the RSS template in PaperMod shows summaries instead of the full articles, but a switch in the TOML or YAML site config file can change that. I dislike YAML, so the example here is in TOML:

[params]
ShowFullTextinRSS = true

To be clear, the template I derived in the previous section isn’t affected by this switch. This is intended for the default PaperMod installation.

If somehow the RSS template you use (e.g. in other themes) doesn’t support showing the full article, you’ll have to modify the template yourself by replacing {{ .Summary | ...}} with {{ .Content | ...}} within the context of each page and work up from there. See the previous section as for how this can be implemented.

Absolute URLs and Entry IDs

The URLs of posts should already be in the absolute form. That’s kind of common sense. I haven’t come across any RSS feed that uses relative URLs for the posts. My setup also by default uses the absolute URL of a post as its entry ID. This part is fine.

But what about links inside the articles?

In the very first post of this blog, there is a relative link referring to the site itself. It showed up as a relative link in the RSS feed. Sure, Atom has specified how relative links should work—but it kind of depends on the implementation to do it right. My FreshRSS did it okay. I’m not sure about other RSS readers, though.

Actually, the reason the link comes out as a relative one is because I used the relref shortcode provided by Hugo. Since this site only has one domain name, and the HTML is regenerated each deployment, there’s no actual reason to put a relative link there. Simply replace relref with ref shortcode fixed the spooky relative link inside my RSS feed.

Content Type

There are two parts regarding content types:

  1. How the feed is referenced in the generated HTML.
  2. What the reader client gets as the HTTP “Content-Type” header.

The PaperMod template for page headers referenced the feed using Hugo’s built-in media type system(MIT-licensed):

{{- /* RSS */}}
{{ range .AlternativeOutputFormats -}}
<link rel="{{ .Rel }}" type="{{ .MediaType.Type | html }}" href="{{ .Permalink | safeURL }}">
{{ end -}}

In Hugo’s default RSS configuration, this will be rendered as something like this:

<link rel="alternate" type="application/rss+xml" href="https://citrinefox.com/index.xml">

Since we have changed the feed format from RSS to Atom (while still using RSS as a general umbrella term), we should also update Hugo’s knowledge on what we mean by “RSS” in the main site config file:

[mediaTypes]
[mediaTypes.'application/atom+xml']
suffixes = ['xml']

[outputFormats]
[outputFormats.RSS]
mediaType = 'application/atom+xml'

The second part is actually unrelated to Hugo unless you use Hugo’s development server in production—don’t do that!

This site is hosted on Cloudflare Pages. Cloudflare Pages automagically put “application/xml” in the “Content-Type” header when it sees an XML file. This works just fine. Though, we can make it more specific easily with Cloudflare’s Transform Rules.

To do this, navigate to the domain name’s “Transform Rules” configuration page, switch to “Modify Response Header” tab, then create a new rule here, as shown in the following two screenshots.

Cloudflare screenshot 1

Cloudflare screenshot 2

Final Check

As a final note, you can put your feed URL into W3C’s feed validation service to see if the feed is indeed okay, and to see if the feed has any other issues.