In a Static Next.js RSS Feed, Check XML Escaping First
When a static Next.js site generates its own RSS feed, XML escaping is one of the easiest things to miss.
Article titles and descriptions can contain &, <, >, quotes, or apostrophes. If those values are inserted directly into feed.xml, the feed can become invalid XML. A browser may still show something, but RSS readers and parsers may fail.
If your feed is generated from Markdown frontmatter, treat those fields as input data and escape them.
RSS is XML, not HTML
RSS 2.0 is XML. It is not a forgiving HTML document.
This title is ordinary text:
Next.js & Cloudflare Pages deployment guide
But this XML is invalid:
<title>Next.js & Cloudflare Pages deployment guide</title>
The ampersand must be escaped:
<title>Next.js & Cloudflare Pages deployment guide</title>
The same applies to <, >, quotes, and apostrophes when they are placed inside XML text.
Always pass template values through escapeXml
It is common to build an RSS route with template strings:
const item = `
<item>
<title>${article.title}</title>
<link>${url}</link>
<description>${article.description}</description>
</item>
`;
This works until an article contains XML-sensitive characters.
Use an XML escape function:
function escapeXml(value: string): string {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
Then use it for every interpolated value:
const item = `
<item>
<title>${escapeXml(article.title)}</title>
<link>${escapeXml(url)}</link>
<description>${escapeXml(article.description)}</description>
</item>
`;
Even if the content comes from your own Markdown files, the RSS generator should treat it as input.
CDATA is not a universal fix
CDATA can be useful:
<description><![CDATA[Next.js & Cloudflare Pages]]></description>
But it has its own edge cases. If the content includes ]]>, the CDATA section closes. Mixing escaped XML fields with CDATA fields can also make the generator harder to reason about.
For short metadata fields such as title and description, normal XML escaping is usually simpler and safer.
Static export freezes the feed at build time
With output: "export", the feed is generated at build time and deployed as static output.
That means a broken feed.xml stays broken until the next build. There is no server-side code that can fix it after deployment.
For the broader static-export mental model, see 5 Pitfalls of Next.js output: export and How to Avoid Them, especially the section on treating RSS and sitemap as build-time static files.
After adding an article, check:
out/feed.xmlexists- the new article appears
- URLs are absolute
- Japanese and English article paths are correct
- special characters are escaped
- the file parses as XML
Browser display is not enough
Opening /feed.xml in a browser is a useful smoke test, but it is not enough. Browsers may display malformed XML in ways that hide the problem.
At minimum, inspect the generated file:
import { readFileSync } from "node:fs";
const xml = readFileSync("out/feed.xml", "utf8");
if (!xml.includes("<rss")) {
throw new Error("feed.xml does not look like RSS");
}
if (xml.includes(" & ")) {
throw new Error("Possible unescaped ampersand in feed.xml");
}
This is not a complete XML validator, but it catches common mistakes. For stronger verification, parse the feed with an XML parser.
Frontmatter fields to watch
RSS usually includes these Markdown frontmatter values:
titledescriptionslugdatelang
The risky fields are title and description because they are natural language and may contain punctuation.
title: "Next.js & RSS: XML escaping basics"
description: "Why <title> and & can break an RSS feed."
Authors should be allowed to write natural titles. The generator should handle escaping.
Summary
When a static Next.js site generates RSS, XML escaping should be part of the feed generator from the beginning.
Do not insert Markdown frontmatter directly into XML template strings. Escape titles, descriptions, URLs, and any other interpolated values. After adding content, verify feed.xml and sitemap.xml, not only the article page.