We built this journal so that an article enters the repository once and leaves the build as a web page and an RSS item. The interesting part sits between those outputs, because a string that is harmless inside a paragraph can change meaning when a serializer treats it as markup.

Our implementation reads structured article files during the build, then gives the same published list to the homepage and the journal index. The feed receives that list too. That removes a second publishing step where a title change reaches the website but leaves subscribers reading the previous version.

Text becomes HTML and HTML becomes XML

We can follow the boundary using a synthetic title containing an angle bracket and an ampersand. When those characters belong to text, a browser needs them encoded. An RSS reader needs the resulting HTML encoded again when it sits inside an XML text node.

const title = 'Notes on <script> & "quotes"';
const html = `<p>${escapeXml(title)}</p>`;
const xml = `<content:encoded>${escapeXml(html)}</content:encoded>`;

The first pass turns the angle brackets in the title into HTML entities. The second pass protects the paragraph markup and those entity strings during XML parsing. After an RSS reader parses the XML, it receives HTML whose paragraph contains the original title as text.

Think of the two encodings as envelopes inside a shipping box. Opening the outer box exposes the envelope, but does not remove the envelope itself. Each parser consumes the encoding that belongs to its own boundary and leaves the next representation ready for its intended consumer.

We keep the article blocks restricted to paragraphs and headings and literal code so that a title containing an angle bracket remains text on the website and in the feed even when the reader decodes the XML and then asks an HTML renderer to display the item.

Three assertions beat a reassuring glance

The downloadable example checks three properties using synthetic strings. First, we assert that the HTML has the expected entities. Then we check that the XML preserves the encoded angle brackets through the next parsing step. The final XML contains zero raw script elements even though the input includes text shaped like one.

That gives us two encoding boundaries and three assertions without introducing an HTML sanitizer into the article format (one less component to configure). It also gives code examples a predictable representation because the renderer treats their contents as literal text. Angle brackets get to keep their day job.

Publication belongs to the build

For item identifiers, we use stable article URLs and sort posts by publication date. Drafts and future dates stay outside the published list. The site exports the feed to a file alongside the article pages so serving a subscription requires zero application processes after deployment.

That decision trades immediate editing for a single deployable artifact. After the next build and deployment, the source change reaches readers. We keep the article page and its RSS representation in that same artifact because the publication boundary is easier to inspect when both outputs come from one source.

The next useful test is to put hostile strings through the real feed and parse the result as XML. We include that check in the repository tests so a future change to the renderer has to preserve the text boundary before the site ships.

Sources and examples