<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Atom on ln --help</title>
    <link>https://blog.mei-home.net/tags/atom/</link>
    <description>Recent content in Atom on ln --help</description>
    <generator>Hugo -- 0.152.2</generator>
    <language>en</language>
    <lastBuildDate>Mon, 05 Jan 2026 22:15:49 +0100</lastBuildDate>
    <atom:link href="https://blog.mei-home.net/tags/atom/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>FreshRSS: An RSS/Atom Feed Reader</title>
      <link>https://blog.mei-home.net/posts/freshrss/</link>
      <pubDate>Mon, 05 Jan 2026 22:15:49 +0100</pubDate>
      <guid>https://blog.mei-home.net/posts/freshrss/</guid>
      <description>I try Nextcloud News with RSSGuard as well as FreshRSS, ending on FreshRSS</description>
      <content:encoded><![CDATA[<p>Wherein I end up replacing my Brief setup for RSS with FreshRSS.</p>
<p>Over the holidays, I visited my family and only had my laptop with me. While I
have most things properly synced, my RSS feed subscriptions are not. Up to now,
I&rsquo;ve been using the <a href="https://addons.mozilla.org/de/firefox/addon/brief/">Brief Firefox extension</a>.
It looks like this:
<figure>
    <img loading="lazy" src="brief.png"
         alt="A screenshot of Brief&#39;s UI. On the left is the menu, with options for configuration, updating feeds and so on. Below that are menu entries for different views, showing all posts, only today&#39;s, bookmarked posts or trashed posts. Below that are all the blogs I&#39;m subscribed to, ranging from my own blog, over some corporate blogs like Turing Pi&#39;s to those of friends and acquaintances from the Fediverse. To the right of that menu are the current posts, divided into sections by publishing date. It&#39;s overall a very simple UI. The list of current posts only shows the headlines, no content."/> <figcaption>
            <p>Example of the Brief UI</p>
        </figcaption>
</figure>
</p>
<p>And it was fine. I really don&rsquo;t need much from an RSS reader. I don&rsquo;t tend to
read posts in my feed reader at all, it&rsquo;s really just an aggregator for me. When
the headline interests me, I read the article on the original page.</p>
<p>The big problem with Brief was the fact that occasionally, I would be on the road,
and hence away from my desktop. And I would not have all of my blogs around to
read. Which isn&rsquo;t <em>that</em> annoying from the perspective of not having the current
reading state of individual articles around. But rather the issue is that I also
didn&rsquo;t have my subscriptions synced on my desktop and laptop setups.</p>
<h2 id="nextcloud-news">Nextcloud News</h2>
<p>Writing a Fediverse post about my woes, <a href="https://transitory.social/@rachel">Rachel</a>
noted that Nextcloud has an RSS reader with <a href="https://apps.nextcloud.com/apps/news">Nextcloud News</a>,
which could safe me some setup compared to standalone solutions like <a href="https://miniflux.app">Miniflux</a>.</p>
<p>The install is pretty simple, but I hit a problem due to the way I&rsquo;m handling
Nextcloud&rsquo;s cron. As I&rsquo;ve noted in my <a href="https://blog.mei-home.net/posts/k8s-migration-19-nextcloud/">Nextcloud setup post</a>,
I&rsquo;m using the <a href="https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/background_jobs_configuration.html#webcron">Webcron</a>
option, with a separate container which regularly hits the required endpoint and
triggers Nextcloud&rsquo;s background jobs. But this was a problem for the setup of
News. As per its docs, it cannot work with Webcron. That&rsquo;s because News has to
run the feed fetching via the cron setup, and remote content fetching can take
a while. So it&rsquo;s restricted to using a normal cron job. I took this chance to
finally dig deep enough into my setup to be able to use cron properly.</p>
<p>But before I did so, I had a look at the cron option <a href="https://github.com/nextcloud/news-updater">offered by the News app</a>.
It&rsquo;s a Python script which does the feed updates. I disregarded this option
because it seems to require a Nextcloud admin account.</p>
<p>Next, I looked at options to run Nextcloud&rsquo;s cron with a real cron job. This is
famously complicated in a containerized setup, but Nextcloud provides an example
in <a href="https://github.com/nextcloud/docker/blob/master/.examples/docker-compose/with-nginx-proxy/postgres/fpm/compose.yaml">their docker-compose</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  <span style="color:#f92672">cron</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">nextcloud:fpm-alpine</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">nextcloud:/var/www/html:z</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># NOTE: The `volumes` config of the `cron` and `app` containers must match</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">entrypoint</span>: <span style="color:#ae81ff">/cron.sh</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">db</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">redis</span>
</span></span></code></pre></div><p>Reproducing this setup in Nextcloud&rsquo;s Pod resulted in this error:</p>
<pre tabindex="0"><code>crond: can&#39;t set groups: Operation not permitted
</code></pre><p>So I&rsquo;d have to run the container with <code>root</code> permissions. Instead of doing that,
I decided to just re-write my original web cron script a little bit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">: Launched task, sleeping for </span><span style="color:#e6db74">${</span>INITIAL_WAIT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>INITIAL_WAIT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">while</span> true; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  php -f /var/www/html/cron.php 2&gt;&amp;<span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">: Sleeping for </span><span style="color:#e6db74">${</span>SLEEPTIME<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>  sleep <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SLEEPTIME<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><p>That container then got all of the mounts and env variables of my main Nextcloud
container, and now I&rsquo;ve got Nextcloud&rsquo;s cron running via this cron job, instead
of Webcron.</p>
<p>The web interface looks like this:</p>
<figure>
    <img loading="lazy" src="weird-sorting-youtube-folder.png"
         alt="A screenshot of Nextcloud New&#39;s web UI. At the top are the typical Nextcloud main menu options, e.g. dashboard or files or photos. In the main area on the left is a menu. At the top is a large &#39;subscribe&#39; button for adding new feeds. Below it is a button for new folders. After that come the different views for feeds. At the top is one with all unread articles. Then one for all articles, followed by &#39;viewed last&#39; and then &#39;bookmarked&#39;. Then come my subscribed feeds, sorted by folders. The selected feed is the &#39;YouTube&#39; folder, showing 133 unread items. To the right of the menu is the main area, showing a list of articles. Or rather, in this particular case, YouTube videos. One important thing to note is that the list only contains the video&#39;s titles, but not which feed it is coming from. Another important thing is that videos are not sorted by publishing date, as videos published on October 24th are directly followed by videos from 6 days ago or yesterday. Those in turn are then followed by videos from November 17th and three weeks ago."/> <figcaption>
            <p>Example of the Nextcloud News UI.</p>
        </figcaption>
</figure>

<p>There are two things I didn&rsquo;t really like. One is that the feed an article is
coming from isn&rsquo;t shown in the list. That should not matter too much for most use
cases, because the favicon is still shown. But starting to use YouTube&rsquo;s RSS feeds
was one of the things I wanted to do, and of course all of those feeds would
just have YouTube&rsquo;s favicon.</p>
<p>Also note the order of the videos. They&rsquo;re not ordered purely by publishing date.
Instead, the order seems to be first by feed, and only then by publishing date.
Which for me ruins the usability of combined feeds like the YouTube folder here.
This is a <a href="https://github.com/nextcloud/news/issues/2626">known issue</a>, and seems
to be related to the architecture of the News app if I&rsquo;m reading the issue&rsquo;s
comments correctly.</p>
<p>At the same time, those two problems seemed to be only related to the UI. So I
decided to look around for a desktop client for RSS. I ultimately landed on
<a href="https://github.com/martinrotter/rssguard">RSSGuard</a>.</p>
<p>It works nicely with Nextcloud News and can properly sync feeds and the read/unread
state of articles. One thing I&rsquo;m not sure about whether it&rsquo;s me being a bit
incompetent, but it looked like adding feeds was not possible in RSSGuard, only
via the News web interface.</p>
<p>RSSGuard looks like this:</p>
<figure>
    <img loading="lazy" src="rss-guard.png"
         alt="A screenshot of RSSGuard. Its UI has a similar layout as Nextcloud News. On the left is a list of the feeds I&#39;m subscribed to, sorted into folders. They&#39;re arranged under my Nextcloud account, indicating that RSSGuard supports multiple feed aggregator accounts. On the right is the lift of articles. It shows the title, author and a date. In this case, the screenshot shows the Practical Engineering YouTube channel&#39;s recent videos. Arranged along the top are buttons for bookmarking an article, as well as marking it read/unread."/> <figcaption>
            <p>Example of RSSGuard</p>
        </figcaption>
</figure>

<p>I liked this interface a bit better than News&rsquo; web UI. The main issue here was
that I don&rsquo;t really like separate apps for things these days. For most things,
I&rsquo;d rather prefer a nice web interface.</p>
<p>In addition, I also realized another annoying thing about Nextcloud News. It seems
that it uses the &ldquo;Last updated&rdquo; date for article dates, not the published date.
This, too, I find pretty annoying. Take for example the topmost video in the
above screenshot. It&rsquo;s <a href="https://www.youtube.com/watch?v=3nDdLiXS5wk">this one</a>.
The date shown by both, Nextcloud News and RSSGuard, is 2025-12-24. But the
video was actually published on 2025-10-07. I looked around a lot, and couldn&rsquo;t
find an option to switch to always using the publishing date, not the date the
article was last updated.</p>
<p>This finally put me off Nextcloud News.</p>
<h2 id="freshrss">FreshRSS</h2>
<p>Looking at other options, I finally decided on <a href="https://freshrss.org/index.html">FreshRSS</a>.</p>
<p>It&rsquo;s written in PHP and supplies a container for deployments out of the
box. It also supports OIDC for SSO and works nicely with my Keycloak instance.
For data storage, it supports all the mainstream ones, including MySQL, PostgreSQL
and SQLite. As I&rsquo;m not foreseeing much load, I decided on staying with SQLite.
Besides the database, it also needs some space for stuff like cached favicons.</p>
<p>The container already comes with an Apache instance, so no further web server
for delivering static assets is required. The container also comes with a cron
daemon, so there&rsquo;s no need for setting up a separate process for triggering the
feed update.</p>
<p>The setup in my Kubernetes cluster was pretty straightforward, so I will only
provide the Deployment manifest here:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">freshrss</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">homelab/app</span>: <span style="color:#ae81ff">freshrss</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">strategy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#e6db74">&#34;Recreate&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">homelab/app</span>: <span style="color:#ae81ff">freshrss</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">automountServiceAccountToken</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">fsGroup</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">freshrss</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">freshrss/freshrss:{{ .Values.appVersion }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">freshrss</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/www/FreshRSS/data</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">subPath</span>: <span style="color:#ae81ff">data</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">freshrss</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/www/FreshRSS/extensions</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">subPath</span>: <span style="color:#ae81ff">extensions</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">200m</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">500Mi</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">TZ</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;Europe/Berlin&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CRON_MIN</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;2,32&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">LISTEN</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;0.0.0.0:8080&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">FRESHRSS_ENV</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;production&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># My main Traefik instance as well as my k8s Pod CIDR</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">TRUSTED_PROXY</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;10.1.1.1 10.2.0.0/16&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">OIDC_ENABLED</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">OIDC_PROVIDER_METADATA_URL</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;https://login.example.com/realms/example/.well-known/openid-configuration&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">OIDC_REMOTE_USER_CLAIM</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;preferred_username&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">OIDC_SCOPES</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;openid profile&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">OIDC_X_FORWARDED_HEADERS</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;X-Forwarded-Host X-Forwarded-Port X-Forwarded-Proto&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">OIDC_CLIENT_ID</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">oidc-secret</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">id</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">OIDC_CLIENT_SECRET</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">oidc-secret</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">secret</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">OIDC_CLIENT_CRYPTO_KEY</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">oidc-encrypt-key</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">secret</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">freshrss-http</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">TCP</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">freshrss</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">persistentVolumeClaim</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">claimName</span>: <span style="color:#ae81ff">freshrss-volume</span>
</span></span></code></pre></div><p>Similar to what I wrote above on Nextcloud and cron, the FreshRSS container
needs to run as root, as it is running a cron daemon. The Apache instance drops
privileges and uses the <code>www-data</code> user with UID 33 though.</p>
<p>The <code>CRON_MIN</code> configuration configures the cronjob updating feeds to run
every 30 minutes, at hh:02 and hh:32.</p>
<p>Upon first visiting the FreshRSS URL, it will show a few setup pages for
configuring the initial user/admin account and the database. When using OIDC for
authentication, some care has to be taken: The username for the new user needs
to be the same as the OIDC username. The relevant docs can be found <a href="https://freshrss.github.io/FreshRSS/en/admins/16_OpenID-Connect.html">here</a>.
The password provided on the page is not relevant, as it won&rsquo;t be used when
OIDC auth is enabled.</p>
<p>As Keycloak is not among the documented OIDC providers in the FreshRSS docs,
here is a short overview of the config which worked for me for configuring the
client in Keycloak:</p>
<ul>
<li><em>Root URL:</em> <a href="https://freshrss.example.com">https://freshrss.example.com</a></li>
<li><em>Home URL:</em> <a href="https://freshrss.example.com">https://freshrss.example.com</a></li>
<li><em>Valid Redirect URIs:</em> <a href="https://freshrss.example.com:443/i/oidc">https://freshrss.example.com:443/i/oidc</a>*
<ul>
<li>Weirdly enough, the port is necessary here, as the FreshRSS container does
provide the redirect URL exactly like this. Without the port, Keycloak will
reject the request</li>
</ul>
</li>
<li><em>Valid post logout redirect URIs:</em> <a href="https://freshrss.example.com/">https://freshrss.example.com/</a>*</li>
<li><em>Web origins:</em> <a href="https://freshrss.mei-home.net">https://freshrss.mei-home.net</a></li>
<li><em>Client Authentication:</em> On</li>
<li><em>Authorization:</em> Off</li>
<li><em>Standard Flow:</em> On</li>
<li>All other check boxes off</li>
</ul>
<h2 id="adding-feeds">Adding Feeds</h2>
<p>Once the install was complete, I could start adding feeds. This is what FreshRSS'
UI looks like:</p>
<figure>
    <img loading="lazy" src="freshrss-ui.png"
         alt="A screenshot of FreshRSS&#39; web UI. It is split into two parts, a menu with the feeds on the left, and the main area with the currently selected feeds on the right. At the top of the menu on the left is a big button for subscription management and adding feeds. Below it are several views of the available feeds. At the top is a &#39;Main stream&#39;, which shows ten unread articles via a number next to it. Below it are important feeds and favourited articles, both without content at the moment. Below is the &#39;Uncategorized&#39; category. It contains one feed, called &#39;FreshRSS releases&#39;, also showing 10 unread articles. This feed is currently selected. At the top of the main area on the right are some buttons. The first two allow switching between showing unread or already read articles. Next come two buttons for showing favourited or un-favourited articles. Then comes a button for choosing configurable user-quieries. Next is a dropdown menu with some actions for the whole feed: Marking all articles as read, and marking articles older than one day or older than one week as read. Finally , there are buttons for changing the main view to a more or less detailed view. And finally, there&#39;s a button for manually triggering an update for the specific feed. The main area contains the list of articles. At the top of the list is a line saying &#39;Received today -- 4 January 2026&#39;. Each line first contains a button to mark the article as read/unread and the another one for favouriting the article. Next comes the name of the feed, which is also a link for opening that feed in FreshRSS. Next comes the article&#39;s headline. In this case, it&#39;s the subject line of the GitHub release, e.g. &#39;FreshRSS 1.26.2&#39;. Finally follows the publishing date of the article and a button to open the article on the original page. Below the list of articles is a short note that there are no more articles, followed by a very big button which will mark all articles as read and jump to the next unread sibling feed."/> <figcaption>
            <p>Example of the brief UI right after finishing the setup.</p>
        </figcaption>
</figure>

<p>The &ldquo;FreshRSS releases&rdquo; is a GitHub releases RSS feed for FreshRSS which is added
by default for all new users.</p>
<p>Note the &ldquo;Received today &ndash; 4 January 2026&rdquo; line at the top. I don&rsquo;t really like
this, as I don&rsquo;t really care when an article was fetched, but rather when it was
published. This can be changed via dropdown:</p>
<figure>
    <img loading="lazy" src="freshrss-sorted-publishing.png"
         alt="The same view as before. But now, the menu for setting up the view is expanded at the top, showing a number of options for changing how the article list is shown. It allows sorting by a number of characteristics, e.g. by publication date, user modified date, content length, lexically by the full link, by title, by the feed&#39;s title, and even in random order. The publication date option is selected in the screenshot. The effect on the article list is that now, instead of just one headline saying &#39;Received today -- 4 January 2026&#39;, there is now one headline for every day with a release. E.g. there&#39;s now a headline &#39;Published -- 3 June 2025&#39;, with the article for the FreshRSS 1.26.3 release below it."/> <figcaption>
            <p>Switching to sorting the posts by publication date.</p>
        </figcaption>
</figure>

<p>Addition of a new feed works through the &ldquo;+&rdquo; at the top of the menu. It leads
to this form:
<figure>
    <img loading="lazy" src="freshrss-add-feed.png"
         alt="A screenshot of FreshRSS&#39; subscription management UI. On the left is a menu again, providing access to Subscription management, label management, import/export of data as well as some statistics. It also has a menu item called &#39;Add a feed or category&#39;, which is currently selected. In the main area on the right are multiple forms. The first one is headed &#39;Add a category&#39;, which contains a single field labeled &#39;category&#39; and a button labeled &#39;Add&#39; below it. Next comes the &#39;Add a feed&#39; form. IT has a field labeled &#39;Feed URL&#39;. Then comes a dropdown to chose the category the new feed should be sorted into. That&#39;s followed by two hidden sections with additional config options, labeled &#39;Type of feed source&#39; and &#39;Advanced&#39;. Below that is another &#39;Add&#39; button. Finally, there is the &#39;Add dynamic OPML&#39; form. It has two fields, the first one labeled &#39;OPML category name&#39; and second one called &#39;OPML URL&#39;. That&#39;s again followed by an &#39;Add&#39; button."/> <figcaption>
            <p>The feed and category addition UI.</p>
        </figcaption>
</figure>
</p>
<p>In the &lsquo;Add a feed&rsquo; form, the &lsquo;Feed URL&rsquo; doesn&rsquo;t need to be the full URL of the
feed&rsquo;s XML file. FreshRSS can scan for the typical RSS links. E.g. when adding
my blogs home page into the field, it doesn&rsquo;t have any problem finding the
correct RSS URL at <a href="https://blog.mei-home.net/index.xml">https://blog.mei-home.net/index.xml</a>.</p>
<p>The &ldquo;Type of feed source&rdquo; section contains additional options, which allow for
scraping a website which doesn&rsquo;t provide an RSS feed and adding that to FreshRSS,
but I haven&rsquo;t tried that myself.</p>
<p>The &ldquo;Advanced&rdquo; section contains additional options, like setting additional
headers to be send while fetching the feed or setting credentials for auth.</p>
<p>I don&rsquo;t want to make this post any longer than it is already going to be, so I
will provide all the sites I subscribe to in a follow-up. But I wanted to note
two things. First, GitHub provides RSS feeds on the release pages of projects,
as the FreshRSS feed already demonstrates.
And I&rsquo;m also using YouTube&rsquo;s feeds. They provide an RSS feed per channel, and
I&rsquo;m now using that instead of YouTube&rsquo;s subscriptions page. The one thing I&rsquo;m
missing are the video durations. E.g. when cooking, I like to put on a longer
video to listen to. But I can&rsquo;t see the durations in FreshRSS, as they&rsquo;re not
provided as part of the RSS feeds.
Another annoying thing is that the feeds cannot be filtered to only proper videos.
You also get the shorts when subscribing to a channel. This annoys me a bit,
but luckily most of the channels I&rsquo;m following don&rsquo;t do a lot of shorts. I&rsquo;m
also going to have a look at FreshRSS&rsquo; filtering functionality. I&rsquo;m pretty sure
that it should be possible to filter the shorts via that feature.</p>
<h2 id="open-sourcery">Open Sourcery</h2>
<p>While working on setting up FreshRSS, I was again reminded why I love Open Source.
One of the blogs I read wasn&rsquo;t getting added to FreshRSS. When trying to add it,
I was getting this error in the logs:</p>
<pre tabindex="0"><code>A feed could not be found at `https://blog.example.com/index.xml`; the status code is `200` and content-type is `` [https://blog.example.com/index.xml]
</code></pre><p>That was pretty weird, for two reasons: One, Brief didn&rsquo;t have any issues adding
this blog and handled it perfectly fine. And two, the blog is set up very similar
to mine - running Hugo, even with the same theme, and backed by a Ceph S3 bucket,
fronted by a Traefik instance. Even the Traefik setups are pretty similar. And
yet, my blog worked fine in FreshRSS, and the other blog also worked fine in Brief.</p>
<p>The next thing I tried was appending <code>#force_feed</code> to the feed URL, as proposed
in some FreshRSS issues for cases where the feed wasn&rsquo;t getting added properly.
That resulted in an error again, but this time with a different message:</p>
<pre tabindex="0"><code>A feed could not be found at `https://blog.example.com/index.xml`. Empty body. [https://blog.example.com/index.xml#force_feed]
</code></pre><p>Empty body? I went ahead and curl&rsquo;ed the <code>index.xml</code>. It worked perfectly fine,
no complaints. The content also looked fine. I verified that with the
<a href="https://validator.w3.org/feed/">W3C Feed Validator</a>, and while it showed a few
warnings, it didn&rsquo;t have any major issues with the feed either.</p>
<p>Checking the cURL output a few more times, I started comparing it to the output
for my blog - as I said, our setups are pretty similar. And I finally found the
one major difference: The blog which wasn&rsquo;t working in FreshRSS was sending
a <code>Content-Encoding: aws-chunked</code> header, while mine wasn&rsquo;t. And looking at that
header&rsquo;s <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding">docs</a>,
it seemed to be intended to indicate the compression algorithm used. And <code>aws-chunked</code>
wasn&rsquo;t among the normal values allowed for that header.</p>
<p>I assumed that the issue was somehow related to the fact that the blog was delivered
from a Ceph S3 bucket, but wasn&rsquo;t able to figure out anything more. But I did
wonder why curl&rsquo;ing on the command line worked without issue, but FreshRSS had
problems. And here is why I love Open Source software: Instead of only being able to file
an issue with the project, I was able to check what&rsquo;s wrong myself.</p>
<p>FreshRSS has good <a href="https://freshrss.github.io/FreshRSS/en/developers/02_First_steps.html">developer documentation</a>.
I cloned the repository, and then launched a test instance like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>podman run --rm<span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 8080:80<span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e FRESHRSS_ENV<span style="color:#f92672">=</span>development<span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Paris<span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e <span style="color:#e6db74">&#39;CRON_MIN=1,31&#39;</span><span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>:/var/www/FreshRSS<span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v freshrss_data:/var/www/FreshRSS/data<span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name freshrss<span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  freshrss/freshrss:edge
</span></span></code></pre></div><p>I don&rsquo;t speak PHP at all, but I was still able to litter a few print statements
around the code, and finally figured out that after trying to fetch the <code>index.xml</code>,
the body of the response was indeed empty. That&rsquo;s why the initial attempt said
that there was no feed found, and why the attempt with <code>#force_feed</code> showed an
<code>Empty Body</code> issue.</p>
<p>Then I looked at the actual fetching code <a href="https://github.com/FreshRSS/FreshRSS/blob/fdd82820f16733b6e07def5b590fd94879e5a520/lib/simplepie/simplepie/src/File.php#L89">here</a>. The interesting part was <a href="https://github.com/FreshRSS/FreshRSS/blob/fdd82820f16733b6e07def5b590fd94879e5a520/lib/simplepie/simplepie/src/File.php#L146-L154">this</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php5" data-lang="php5"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">curl_errno</span>($fp) <span style="color:#f92672">===</span> <span style="color:#a6e22e">CURLE_WRITE_ERROR</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">curl_errno</span>($fp) <span style="color:#f92672">===</span> <span style="color:#a6e22e">CURLE_BAD_CONTENT_ENCODING</span>) {
</span></span><span style="display:flex;"><span>    $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">error</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;cURL error &#39;</span> <span style="color:#f92672">.</span> <span style="color:#a6e22e">curl_errno</span>($fp) <span style="color:#f92672">.</span> <span style="color:#e6db74">&#39;: &#39;</span> <span style="color:#f92672">.</span> <span style="color:#a6e22e">curl_error</span>($fp); <span style="color:#75715e">// FreshRSS
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">on_http_response</span>($responseBody <span style="color:#f92672">===</span> <span style="color:#66d9ef">false</span> <span style="color:#f92672">?</span> <span style="color:#66d9ef">false</span> <span style="color:#f92672">:</span> $responseHeaders <span style="color:#f92672">.</span> $responseBody, $curl_options);
</span></span><span style="display:flex;"><span>    $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">error</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>; <span style="color:#75715e">// FreshRSS
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">curl_setopt</span>($fp, <span style="color:#a6e22e">CURLOPT_ENCODING</span>, <span style="color:#e6db74">&#39;none&#39;</span>);
</span></span><span style="display:flex;"><span>    $responseHeaders <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>    $responseBody <span style="color:#f92672">=</span> <span style="color:#a6e22e">curl_exec</span>($fp);
</span></span><span style="display:flex;"><span>    $responseHeaders <span style="color:#f92672">.=</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\r\n</span><span style="color:#e6db74">&#34;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>In my tests, FreshRSS runs into this <code>if</code> condition, with the <code>CURLE_BAD_CONTENT_ENCODING</code>.
Printing the <code>$this-&gt;error</code> value gives this result:</p>
<pre tabindex="0"><code>cURL error 61: Unrecognized content encoding type. libcurl understands deflate, gzip, br, zstd content encodings
</code></pre><p>Checking further and printing the <code>$responseHeaders</code> value as well shows that
<code>Content-Encoding</code> header is set here as well:</p>
<pre tabindex="0"><code>HTTP/2 200
accept-ranges: bytes
content-encoding: aws-chunked
content-type: application/rss+xml
date: Tue, 30 Dec 2025 22:50:37 GMT
etag: &#34;xxx&#34;
last-modified: Sat, 13 Dec 2025 21:23:42 GMT
server: Ceph Object Gateway (squid)
x-amz-meta-md5chksum: xxx
content-length: 11242
</code></pre><p>The original intention of this code seemed to be to disable <code>content-encoding</code>
in case there was an encoding error. And the expectation was that that the second
<code>curl_exec</code> call would then be successful. But it just returned the same error
again, and importantly, did not set the body. But crucially to the rest of the
fetching code, it still stores the HTTP return code - which was &ldquo;200&rdquo;. So all
following code assumed that the fetch was successful.</p>
<p>Then I looked at the documentation for the <code>CURLOPT_ENCODING</code> option, which is
set to <code>'none'</code> in the above code. And I found that it was obsoleted by the
<a href="https://curl.se/libcurl/c/CURLOPT_ACCEPT_ENCODING.html">CURLOPT_ACCEPT_ENCODING option</a>
a long time ago. And that <code>'none'</code> wasn&rsquo;t actually a valid value. When this option
is set, cURL will always try to decompress the response, as it will always
assume that it needs to. But also always checks whether it actually has support
for the <code>Content-Encoding</code> value in the response. And if it doesn&rsquo;t it shows the
above error.</p>
<p>But it looked to me like FreshRSS already had this specific branch of the code
to handle specifically this issue, but it did not work (anymore?). Reading through
the option&rsquo;s docs, it seemed that it instead needed <code>null</code> to be set to completely
disable the handling. So I changed the <code>CURLOPT_ENCODING</code> option to be set to
<code>null</code> instead of <code>'none'</code>. And now the feed was added without any issue.</p>
<p>Open source is an absolutely amazing thing.</p>
<p>I also created a ticket on FreshRSS <a href="https://github.com/FreshRSS/FreshRSS/issues/8374">here</a>,
and my fix has already been merged and should find its way into the next FreshRSS
release.</p>
<p>That was a very satisfying investigation. &#x1f642;</p>
<p>Concerning the actual issue with sending the header: After some discussion with
the author of the blog, we were able to figure out that the one difference in
our setup is that I&rsquo;m using <code>s3cmd</code> to push the files generated by Hugo to the
S3 bucket. They&rsquo;re using Hugo&rsquo;s <a href="https://gohugo.io/host-and-deploy/deploy-with-hugo-deploy/">deploy</a>
feature. As best as we could figure out, the AWS SDK used by Hugo automatically
sets the header when pushing to a bucket. AWS S3 then just uses the header during
the PUT operation, but doesn&rsquo;t store the fact that the header was set. So it will
not be returned as part of a response. But Ceph S3 seems to be set up differently,
and when the <code>Content-Encoding</code> header is set during the push, it will also
return it as part of the response to a GET request.</p>
<p>And that&rsquo;s it for this one. I hope you all made it safely into 2026, and I
wish you all a happy new year. &#x1f642;</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
