diff --git a/pelicanconf.py b/pelicanconf.py
index 199399f7..8e6f0870 100644
--- a/pelicanconf.py
+++ b/pelicanconf.py
@@ -37,6 +37,8 @@ def _asset_version() -> str:
SITENAME = 'RivasSec | DevSecOps, Kubernetes, AWS IAM'
SITETITLE = 'RivasSec'
SITESUBTITLE = 'Infrastructure. Security. Insight.'
+# Feed title only (feed_polish plugin); SITENAME stays the page
.
+FEED_TITLE = 'RivasSec'
SITEURL = 'https://rivassec.com'
SITEDESCRIPTION = 'Field notes on infrastructure security, cloud hardening, Kubernetes, IAM, and OSINT by RivasSec.'
OG_IMAGE = 'images/og-default.png'
@@ -90,6 +92,7 @@ def _asset_version() -> str:
'extract_toc',
'img_hygiene',
'md_mirror',
+ 'feed_polish',
]
# related_posts configuration
diff --git a/plugins/feed_polish.py b/plugins/feed_polish.py
new file mode 100644
index 00000000..5ba92f78
--- /dev/null
+++ b/plugins/feed_polish.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+"""
+Feed polish
+===========
+
+A Pelican plugin that fixes two things in the generated Atom/RSS feeds, both
+from the "RSS Feed Best Practices" checklist (kevincox.ca, 2022):
+
+* **Absolute URLs in entry content.** Posts link to each other with relative
+ hrefs (``iam-safe-defaults-fail-loud.html``, ``/tools/iam-blast-radius/``).
+ That is fine on the site, but many feed readers resolve relative URLs in
+ feed content wrongly (or not at all), so the links break in the reader. Every
+ relative ``href``/``src`` in an entry's content and summary is resolved
+ against the entry's own permalink. Fragments (``#x``), ``mailto:``,
+ ``tel:`` and ``data:`` URLs are left alone.
+* **Feed title.** Pelican always titles feeds with ``SITENAME`` (the long
+ ```` used for SEO). ``FEED_TITLE``, when set, replaces that prefix in
+ feed titles only, keeping Pelican's `` - `` suffix on category feeds.
+
+Runs on the ``feed_generated`` signal, so it only touches feeds, never the site
+pages.
+"""
+import re
+from urllib.parse import urljoin
+
+_ATTR_RE = re.compile(r'(\s(?:href|src)\s*=\s*)(["\'])(.*?)\2', re.IGNORECASE | re.DOTALL)
+_KEEP_RE = re.compile(r'^(?:[a-z][a-z0-9+.-]*:|//|#)', re.IGNORECASE)
+
+
+def absolutize(html, base):
+ """Resolve every relative href/src in ``html`` against ``base``."""
+ if not html or not base:
+ return html
+
+ def repl(m):
+ url = m.group(3).strip()
+ if not url or _KEEP_RE.match(url):
+ return m.group(0)
+ return f"{m.group(1)}{m.group(2)}{urljoin(base, url)}{m.group(2)}"
+
+ return _ATTR_RE.sub(repl, html)
+
+
+def polish_feed(context, feed):
+ title = context.get("FEED_TITLE")
+ sitename = context.get("SITENAME", "")
+ if title and sitename:
+ current = feed.feed.get("title", "")
+ if current == sitename or current.startswith(sitename + " - "):
+ feed.feed["title"] = title + current[len(sitename):]
+
+ for item in feed.items:
+ base = item.get("link")
+ for key in ("description", "content"):
+ if item.get(key):
+ item[key] = absolutize(item[key], base)
+
+
+def register():
+ # Imported here so the helpers above stay testable without Pelican installed
+ # (the tests workflow runs stdlib unittest only).
+ from pelican import signals
+
+ signals.feed_generated.connect(polish_feed)
diff --git a/tests/test_feed_polish.py b/tests/test_feed_polish.py
new file mode 100644
index 00000000..4bcfce56
--- /dev/null
+++ b/tests/test_feed_polish.py
@@ -0,0 +1,62 @@
+"""Tests for plugins/feed_polish.py (stdlib unittest; Pelican not required)."""
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "plugins"))
+
+from feed_polish import absolutize, polish_feed # noqa: E402
+
+BASE = "https://rivassec.com/devsecops-guide.html"
+
+
+class _Feed:
+ def __init__(self, title, items):
+ self.feed = {"title": title}
+ self.items = items
+
+
+class AbsolutizeTests(unittest.TestCase):
+ def test_relative_links_become_absolute(self):
+ html = 'x y'
+ out = absolutize(html, BASE)
+ self.assertIn('href="https://rivassec.com/iam-safe-defaults-fail-loud.html"', out)
+ self.assertIn('href="https://rivassec.com/tools/iam-blast-radius/"', out)
+
+ def test_images_and_single_quotes(self):
+ out = absolutize("
", BASE)
+ self.assertIn("src='https://rivassec.com/images/a.png'", out)
+
+ def test_absolute_fragment_and_special_schemes_untouched(self):
+ html = ('ab'
+ 'c
'
+ 'd')
+ self.assertEqual(absolutize(html, BASE), html)
+
+ def test_text_that_mentions_href_is_not_rewritten(self):
+ html = "href=foo.html"
+ self.assertEqual(absolutize(html, BASE), html)
+
+
+class PolishFeedTests(unittest.TestCase):
+ CTX = {"SITENAME": "RivasSec | DevSecOps, Kubernetes, AWS IAM", "FEED_TITLE": "RivasSec"}
+
+ def test_feed_title_and_item_content(self):
+ feed = _Feed(self.CTX["SITENAME"], [{"link": BASE, "description": 'a', "content": None}])
+ polish_feed(self.CTX, feed)
+ self.assertEqual(feed.feed["title"], "RivasSec")
+ self.assertEqual(feed.items[0]["description"], 'a')
+
+ def test_category_feed_keeps_suffix(self):
+ feed = _Feed(self.CTX["SITENAME"] + " - DevSecOps", [])
+ polish_feed(self.CTX, feed)
+ self.assertEqual(feed.feed["title"], "RivasSec - DevSecOps")
+
+ def test_no_feed_title_setting_leaves_title(self):
+ feed = _Feed("Site", [])
+ polish_feed({"SITENAME": "Site"}, feed)
+ self.assertEqual(feed.feed["title"], "Site")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/themes/Flex/templates/partial/feed.html b/themes/Flex/templates/partial/feed.html
index d7f253dc..37d79ab4 100644
--- a/themes/Flex/templates/partial/feed.html
+++ b/themes/Flex/templates/partial/feed.html
@@ -2,6 +2,5 @@
{% endif %}
-{% if FEED_ALL_RSS %}
-
-{% endif %}
+{# Only the Atom feed is advertised in : one feed per content avoids a confusing
+ choice for subscribers. The RSS file is still built and linked in the footer. #}