Some human reflections
The current internet is full of ads, scams, and proselytism. This phenomenon even has a name: “enshittification”, a term coined by Cory Doctorow.
At the beginning of the internet, there were passionate people willing to share knowledge and collaborate on common projects. The web felt more human, more normal.
Today, attention has become a resource to be extracted. Platforms are optimized for engagement, not understanding. Content is buried under advertising, dark patterns, and algorithmic noise.
In this context, I feel that RSS is underestimated — and that it could be a niche, or even a refuge, from this bloated, advertising-driven internet.
Let’s get back to RSS feeds
RSS feels outdated to many, yet it remains one of the most elegant tools the web has ever produced. It is simple, decentralized, and user-controlled.
I believe RSS could be reclaimed by communities. Here are a few arguments:
- Selective consumption: you choose the topics that genuinely interest you.
- Homemade blogs first: not big corporations trying to monetize your attention.
- No endless scrolling: you read what’s new, then you stop.
- Peer-to-peer knowledge: direct connections between writers and readers.
- Simple user experience: no FOMO, no forced continuity, no social-proof mechanics.
RSS doesn’t try to hook you. It doesn’t rank, recommend, or manipulate. It simply delivers content you explicitly asked for.
So let’s take care of our feeds — and maybe, by doing so, take care of the web a little too.
Config customization
All RSS configuration is in config/_default/hugo.yaml.
- Limit the number of articles so as not to overwhelm the feed.
1services:
2 RSS:
3 limit: 10
- Change the default
index.xmltofeed.xmland produce only one feed
1# Change the default index.xml to feed.xml
2outputFormats:
3 RSS:
4 mediatype: "application/rss"
5 baseName: "feed"
6
7outputs:
8 home:
9 - HTML
10 - Offline # required by PWA module for displaying the offline pages.
11 - RSS
12 - SearchIndex # required by search module.
13 - WebAppManifest # required by PWA module to make your site installable.
14 # Default outputs for other kinds of pages (avoid producing RSS unnecessarily).
15 section: ['html']
16 taxonomy: ['html']
17 term: ['html']
Customize the output
To customize the XML of your RSS feed, you need to override the default template. For this, create a file named layouts/_default/rss.xml from the default Hugo rss.xml, then adapt it.
For example, in my case, I display only pages (not categories, authors, etc.) from the posts section. I then display the full content with the first image listed in each post’s front matter:
1 {{- $pages := where (where .Site.Pages ".Section" "posts") "Kind" "page" }}
2 {{- $limit := .Site.Config.Services.RSS.Limit }}
3 {{- if ge $limit 1 }}
4 {{- $pages = $pages | first $limit }}
5 {{- end }}
6 {{- range $pages }}
7 <item>
8 {{- $title := .Title }}
9 <title>{{ .Title }}</title>
10 <link>{{ .Permalink }}</link>
11 <pubDate>{{ .PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
12 {{- with $authorEmail }}<author>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</author>{{ end }}
13 <guid>{{ .Permalink }}</guid>
14 {{- $content := safeHTML (.Content | html) -}}
15 <description>
16 {{- with index .Params.images 0 }}
17 {{- $imageURL := printf "%s%s" $.Site.BaseURL . | absURL }}
18 {{ "<" | html }}img src="{{ $imageURL }}"
19 alt="Featured image for {{ $title }}"
20 width="600" height="400"
21 {{ "/>" | html }}
22 {{- end }}
23 {{ $content }}
24 </description>
25 </item>
26 {{- end }}
Here is a nice RSS optimization from Romain’s blog, which also reminds us of some basic facts:
Beyond standard feed readers, a clean RSS feed is the backbone of automated newsletters. Tools like Mailchimp, ConvertKit, or MailerLite have features called “RSS-to-Email”.
Check and Validate
Validate the RSS feed with the W3C Feed Validation Service.
Check the final output with an RSS reader such as FeedDesk for Windows or FreshRSS for Linux.
Find a blog’s RSS feed here.
Bonus
🐍 As a bonus, here is a Python script that converts a YAML file to an OPML file that I can import into FeedDesk.
1# Example usage:
2# python3 content/posts/howto-customize-feed-rss/codes/yaml-to-opml.py data/rss/blogs.yaml -o blogs.opml --include-inactive
3
4import argparse
5import yaml
6import xml.etree.ElementTree as ET
7from xml.dom import minidom
8from datetime import datetime
9from pathlib import Path
10
11
12def prettify(elem):
13 rough_string = ET.tostring(elem, "utf-8")
14 reparsed = minidom.parseString(rough_string)
15 return reparsed.toprettyxml(indent=" ")
16
17
18def yaml_to_opml(input_path: Path, output_path: Path, include_inactive: bool):
19 with input_path.open("r", encoding="utf-8") as f:
20 feeds = yaml.safe_load(f)
21
22 opml = ET.Element("opml", version="1.0")
23
24 head = ET.SubElement(opml, "head")
25 ET.SubElement(head, "title").text = "RSS Feeds"
26 ET.SubElement(head, "dateCreated").text = datetime.utcnow().isoformat()
27
28 body = ET.SubElement(opml, "body")
29
30 for feed in feeds:
31 if not feed.get("feed"):
32 # Explicit rule: no feed → skip
33 continue
34
35 if not feed.get("active", False) and not include_inactive:
36 continue
37
38 outline = ET.SubElement(
39 body,
40 "outline",
41 text=feed["name"],
42 title=feed["name"],
43 type="rss",
44 xmlUrl=feed["feed"],
45 htmlUrl=feed.get("url", ""),
46 )
47
48 if feed.get("description"):
49 outline.set("description", feed["description"])
50
51 if not feed.get("active", False):
52 outline.set("category", "inactive")
53
54 output_path.write_text(prettify(opml), encoding="utf-8")
55
56
57def main():
58 parser = argparse.ArgumentParser(
59 description="Convert a YAML RSS list to OPML"
60 )
61 parser.add_argument(
62 "input",
63 type=Path,
64 help="Path to input YAML file",
65 )
66 parser.add_argument(
67 "-o",
68 "--output",
69 type=Path,
70 help="Path to output OPML file (default: same name as input)",
71 )
72 parser.add_argument(
73 "--include-inactive",
74 action="store_true",
75 help="Include feeds marked as inactive",
76 )
77
78 args = parser.parse_args()
79
80 if not args.input.exists():
81 parser.error(f"Input file does not exist: {args.input}")
82
83 output_path = args.output or args.input.with_suffix(".opml")
84
85 yaml_to_opml(
86 input_path=args.input,
87 output_path=output_path,
88 include_inactive=args.include_inactive,
89 )
90
91 print(f"✅ OPML written to {output_path}")
92
93
94if __name__ == "__main__":
95 main()
It takes a YAML file with this structure:
1# Name; URL; description; feed; active (true/false)
2- name: Bałtyk Blog
3 url: https://mozebaltyk.github.io/
4 feed: https://mozebaltyk.github.io/feed.xml
5 description: My personal blog about sysadmin and devops topics.
6 active: false
7- name: While True Do
8 url: https://blog.while-true-do.io/
9 feed: https://blog.while-true-do.io/rss/
10 description: A blog about programming, technology, and software development.
11 active: true










