Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pelicanconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <title>.
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'
Expand Down Expand Up @@ -90,6 +92,7 @@ def _asset_version() -> str:
'extract_toc',
'img_hygiene',
'md_mirror',
'feed_polish',
]

# related_posts configuration
Expand Down
64 changes: 64 additions & 0 deletions plugins/feed_polish.py
Original file line number Diff line number Diff line change
@@ -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
``<title>`` used for SEO). ``FEED_TITLE``, when set, replaces that prefix in
feed titles only, keeping Pelican's `` - <category>`` 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)
62 changes: 62 additions & 0 deletions tests/test_feed_polish.py
Original file line number Diff line number Diff line change
@@ -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 = '<a href="iam-safe-defaults-fail-loud.html">x</a> <a href="/tools/iam-blast-radius/">y</a>'
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("<img src='images/a.png' alt=\"a\">", BASE)
self.assertIn("src='https://rivassec.com/images/a.png'", out)

def test_absolute_fragment_and_special_schemes_untouched(self):
html = ('<a href="https://example.com/x">a</a><a href="#sec">b</a>'
'<a href="mailto:me@example.com">c</a><img src="data:image/png;base64,AA">'
'<a href="//cdn.example.com/y">d</a>')
self.assertEqual(absolutize(html, BASE), html)

def test_text_that_mentions_href_is_not_rewritten(self):
html = "<code>href=foo.html</code>"
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 href="a.html">a</a>', "content": None}])
polish_feed(self.CTX, feed)
self.assertEqual(feed.feed["title"], "RivasSec")
self.assertEqual(feed.items[0]["description"], '<a href="https://rivassec.com/a.html">a</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()
5 changes: 2 additions & 3 deletions themes/Flex/templates/partial/feed.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
<link href="{{ FEED_DOMAIN }}/{{ FEED_ALL_ATOM }}" type="application/atom+xml" rel="alternate" title="{{ SITETITLE|default(SITENAME) }} Atom Feed">
{% endif %}

{% if FEED_ALL_RSS %}
<link rel="alternate" type="application/rss+xml" title="{{ SITETITLE|default(SITENAME) }} RSS Feed" href="{{ SITEURL }}/feeds/all.rss.xml">
{% endif %}
{# Only the Atom feed is advertised in <head>: one feed per content avoids a confusing
choice for subscribers. The RSS file is still built and linked in the footer. #}
Loading