Living Document Notice
Published 2026-09-15. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.
Generating Spec-Compliant RSS 2.0 Feeds
Summary
RSS syndication remains an essential protocol for open-web content dissemination and feed aggregators. However, modern full-stack web platforms frequently neglect syndication endpoints, emitting malformed XML, unescaped entity delimiters, invalid date formats, or non-canonical GUID strings that cause feed readers like NetNewsWire, Miniflux, and Feedly to duplicate entries or fail feed parsing altogether.
Harbor integrates a dedicated, spec-compliant RSS 2.0 generator directly into its Hono edge router. By querying published article records from Directus, escaping character entities, formatting dates strictly to RFC 822 specifications, and emitting RFC-compliant guid elements with explicit isPermaLink="true" attributes, Harbor delivers syndication feeds that parse cleanly across all standards-compliant aggregators.
Standards Compliance: RFC 822 Dates and Canonical GUID Identifiers
Syndication parsers enforce strict XML validation rules that differ substantially from forgiving HTML parsers:
- Date Serialization: RSS 2.0 requires pubDate elements to follow RFC 822 (updated by RFC 2822) formatting (
Tue, 15 Sep 2026 12:00:00 GMT). Emitting ISO 8601 strings (2026-09-15T12:00:00Z) violates the specification and causes older desktop aggregators to drop publication timestamps. - Canonical Identifiers: The
<guid>tag must serve as a durable, unchanging identifier. Harbor binds the GUID to the canonical permalink URL while explicitly declaringisPermaLink="true". Modifying article titles or timestamps will not trigger duplicate unread notifications in subscriber feeds. - XML Entity Escaping and CDATA: Raw HTML bodies injected into
<description>or<content:encoded>tags must either escape reserved characters (&,<,>,") or enclose raw content inside unparsed CDATA blocks (<![CDATA[...]]>).
| Element | Specification Requirement | Common Malformed Output | Harbor Spec Implementation |
|---|---|---|---|
pubDate | RFC 822 / 2822 date-time | 2026-09-15 (ISO string) | Tue, 15 Sep 2026 12:00:00 GMT |
guid | Stable canonical string | Random UUID or database auto-increment ID | https://bosunpkm.com/posts/slug |
isPermaLink | Explicit boolean attribute | Missing attribute (assumed true by spec) | Explicit isPermaLink="true" |
docs | Reference to RSS 2.0 specification | Omitted | https://www.rssboard.org/rss-specification |
atom:link | Self-referential rel=“self” link | Omitted or mismatched protocol | Fully qualified canonical feed URL |
Hono Spec-Compliant Feed Route Implementation
The Harbor RSS route streams XML directly with appropriate MIME headers and cache control policies:
import { Hono } from "hono";
interface FeedArticle {
id: string;
title: string;
slug: string;
summary: string;
content_html: string;
published_at: string;
}
const app = new Hono();
app.get("/feed.xml", async (c) => {
const directusUrl = process.env.DIRECTUS_INTERNAL_URL || "http://127.0.0.1:8055";
const token = process.env.DIRECTUS_READ_TOKEN;
const siteUrl = "https://bosunpkm.com";
const res = await fetch(
`${directusUrl}/items/articles?filter[status][_eq]=published&sort=-published_at&limit=25`,
{ headers: { Authorization: `Bearer ${token}` } }
);
if (!res.ok) {
return c.text("Feed Unavailable", 503);
}
const payload = await res.json();
const articles: FeedArticle[] = payload.data || [];
const lastBuildDate = articles.length > 0
? new Date(articles[0].published_at).toUTCString()
: new Date().toUTCString();
const itemsXml = articles.map((post) => {
const permalink = `${siteUrl}/posts/${post.slug}`;
const pubDate = new Date(post.published_at).toUTCString();
return ` <item>
<title>${escapeXml(post.title)}</title>
<link>${permalink}</link>
<guid isPermaLink="true">${permalink}</guid>
<pubDate>${pubDate}</pubDate>
<description><![CDATA[${post.summary}]]></description>
<content:encoded><![CDATA[${post.content_html}]]></content:encoded>
</item>`;
}).join("\n");
const xmlResponse = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Bosun PKM Dispatch</title>
<link>${siteUrl}</link>
<description>Technical dispatches on sovereign local-first systems and edge publishing.</description>
<language>en-us</language>
<lastBuildDate>${lastBuildDate}</lastBuildDate>
<docs>https://www.rssboard.org/rss-specification</docs>
<atom:link href="${siteUrl}/feed.xml" rel="self" type="application/rss+xml" />
${itemsXml}
</channel>
</rss>`;
c.header("Content-Type", "application/xml; charset=UTF-8");
c.header("Cache-Control", "public, max-age=0, s-maxage=300, stale-while-revalidate=900");
return c.body(xmlResponse);
});
function escapeXml(unsafe: string): string {
return unsafe.replace(/[<>&'"]/g, (c) => {
switch (c) {
case "<": return "<";
case ">": return ">";
case "&": return "&";
case "'": return "'";
case '"': return """;
default: return c;
}
});
}
export default app;Feed Validation and Parsing Probing
Verify the generated XML feed against the W3C validator rules and shell validation tools:
# Fetch and validate XML well-formedness with xmllint
curl -s http://127.0.0.1:3000/feed.xml | xmllint --noout - && echo "XML Well-Formed"
# Verify RFC 822 date compliance in item tags
curl -s http://127.0.0.1:3000/feed.xml | grep -E "<pubDate>" | head -n 3
# Verify explicit isPermaLink attribute on GUID tags
curl -s http://127.0.0.1:3000/feed.xml | grep -E '<guid isPermaLink="true">' | head -n 3
# Validate caching headers and content type returned by edge
curl -s -I http://127.0.0.1:3000/feed.xml | grep -E "(Content-Type|Cache-Control)"- Directus Target: harbor
- Garden Source Reference: HAR-1006 - Generating Spec-Compliant RSS 2.0 Feeds, MOC - Harbor Ecosystem, MOC - Local-First Systems and Synchronization, MOC - Bosun PKM Tools