← ./articles-ja

Next.js静的サイトのRSSはXMLエスケープ漏れを先に疑う

Next.jsの静的サイトでRSSフィードを自前生成する場合、意外に壊れやすいのがXMLエスケープです。

記事タイトルやdescriptionに &<>、引用符が入ると、RSS XMLとして不正な文字列になります。ブラウザではなんとなく表示されることもありますが、RSSリーダーや検索エンジンのパーサーでは失敗する可能性があります。

RSSを静的書き出しで生成するなら、まず「XMLとして正しいか」を確認します。

RSSはHTMLではなくXML

RSS 2.0はXMLです。HTMLのように多少壊れていてもブラウザが補正してくれる前提ではありません。

たとえば、記事のdescriptionに次の文字列があったとします。

Next.js & Cloudflare Pages deployment guide

これをそのままXMLに埋め込むと、不正になります。

<description>Next.js & Cloudflare Pages deployment guide</description>

& はXMLではエンティティの開始として解釈されます。正しくは次です。

<description>Next.js &amp; Cloudflare Pages deployment guide</description>

同じように、<&lt;>&gt; に変換します。

自前テンプレートでは必ずescape関数を通す

Next.jsのRoute HandlerでRSSを文字列生成する場合、テンプレート文字列で書くことがあります。

悪い例です。

const item = `
  <item>
    <title>${article.title}</title>
    <link>${url}</link>
    <description>${article.description}</description>
  </item>
`;

記事データが自分のMarkdownから来ている場合でも、これは危険です。将来、タイトルに & が入るだけで壊れます。

最低限、XML用のescape関数を通します。

function escapeXml(value: string): string {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&apos;");
}

使う側は次のようにします。

const item = `
  <item>
    <title>${escapeXml(article.title)}</title>
    <link>${escapeXml(url)}</link>
    <description>${escapeXml(article.description)}</description>
  </item>
`;

ポイントは、RSSに入る外部由来の文字列をすべて通すことです。Markdown frontmatterは「自分で書いたもの」ですが、XML生成側から見ると入力データです。

CDATAに逃げればよいとは限らない

RSSではCDATAを使う方法もあります。

<description><![CDATA[Next.js & Cloudflare Pages]]></description>

ただし、CDATAにも落とし穴があります。本文に ]]> が含まれると閉じてしまいます。また、CDATAを使う場所と使わない場所が混ざると、結局どの値をどう扱うかが分かりにくくなります。

短いメタ情報なら、通常のXMLエスケープで十分です。記事本文のHTMLをRSSに含めるなど、長い内容を扱う場合だけCDATAを検討します。

静的書き出しではビルド時に固定される

output: export のNext.jsサイトでは、RSSはビルド時に静的ファイルとして生成されます。

つまり、RSSの壊れ方もビルド時点で固定されます。記事を1本追加しただけで feed.xml が壊れても、デプロイ後にサーバー側で修正されることはありません。次のビルドまで壊れたRSSが配信されます。

RSSやsitemapを静的書き出しでどう扱うか全体像を先に確認したい場合は、Next.jsのoutput: exportで詰まる5つの罠と回避策 の「RSS・sitemapはビルド時生成の静的ファイル」という章を参照してください。

だからこそ、記事追加後の検証にRSS確認を入れます。

確認するポイントは次です。

  • /feed.xml が生成されている
  • XMLとしてパースできる
  • 新しい記事が含まれている
  • &amp; などのエスケープが正しく出ている
  • URLが絶対URLになっている
  • 日本語記事と英語記事のURLが正しいパスになっている

目視だけでは不十分

ブラウザで /feed.xml を開いて表示されるだけでは十分ではありません。ブラウザは表示のために補正することがあります。

ローカルでは、生成された out/feed.xml をXMLパーサーに通すのが確実です。Node.jsなら、軽い検証スクリプトを用意してもよいです。

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");
}

これは完全なXML検証ではありませんが、よくある事故は拾えます。本格的にはXMLパーサーを使ってパースできるか確認します。

frontmatterで注意する文字

RSSに入りやすいfrontmatterは次です。

  • title
  • description
  • slug
  • date
  • lang

特に titledescription は自然文なので、記号が入りやすいです。

title: "Next.js & RSS: XMLエスケープの基本"
description: "RSSで <title> や & が壊れる理由を解説します。"

このような文字が入っても壊れないように、生成側で処理します。記事を書く人に「記号を使わないで」と求めるより、生成コードで守るほうが運用しやすいです。

まとめ

Next.jsの静的サイトでRSSを自前生成するなら、XMLエスケープは最初に確認すべきポイントです。

RSSはXMLなので、&< をそのまま埋め込むと壊れます。記事データがMarkdown frontmatterから来ていても、RSS生成時には入力として扱い、必ずescape関数を通します。

記事追加後は、ページ表示だけでなく、feed.xmlsitemap.xml も確認すると、SEOや配信まわりの事故を減らせます。