AstroでRSSフィードから記事一覧を取得してCloudflare Pagesで表示してみる
自身のnoteに書いた記事をRSSフィードを利用してAstroに一覧表示するための手順をまとめた記事です。
※RSSを配信しているサービスの利用規約やルールをご確認ください。
設定と完成したコード
設定
- Astro 4.7.1
- Astroの出力モード
hybrid
- Cloudflare Pagesの互換性フラグに
nodejs_compat
を設定 - XMLデータをJSON形式に変換するパッケージ
npm i fast-xml-parser -D
このコードでできること
- 複数のRSSフィードから記事を取得して日付が新しい順に並び替えて10件表示
- RSS 2.0、Atom形式
- 日付をYYYY.MM.DD形式

src/libs/fetchAndProcessRSS.ts
import { XMLParser } from "fast-xml-parser";
// 記事のインターフェースを定義
interface Article {
title: string;
link: string;
thumb: string | null;
description: string;
pubDate: string;
}
// RSSフィードを取得してJSONデータに変換する関数
export async function fetchRSS(url: string): Promise<any> {
try {
const response = await fetch(url); // URLからRSSフィードをフェッチ
const xmlData = await response.text(); // フェッチしたデータをテキストとして取得
const parser = new XMLParser({
ignoreAttributes: false, // 属性を無視しない設定
});
const jsonData = parser.parse(xmlData); // XMLデータをJSONに変換
return jsonData;
} catch (error) {
console.error("Error fetching or parsing RSS feed:", error); // エラーが発生した場合にコンソールに表示
throw error; // エラーをスロー
}
}
// 日付をYYYY.MM.DD形式にフォーマットする関数
function formatDate(date: Date): string {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${year}.${month}.${day}`;
}
// アイテムからサムネイルURLを抽出する関数
function extractThumb(item: any): string | null {
if (item.enclosure && item.enclosure.url) {
return item.enclosure.url;
} else if (item["media:thumbnail"]) {
return item["media:thumbnail"];
} else if (Array.isArray(item.link)) {
const link = item.link.find(
(l: any) => l.rel === "enclosure" || l.type?.startsWith("image")
);
return link ? link.href : null;
}
return null;
}
// RSSアイテムをArticleインターフェースにパースする関数
function parseRSSItem(item: any, isAtom: boolean = false): Article {
return {
title: item.title,
link: isAtom ? item.link?.href || item.link : item.link,
thumb: extractThumb(item),
description: item.description || item.summary || item.content,
pubDate: formatDate(
new Date(item.pubDate || item.updated || item.published)
),
};
}
// RSS 2.0形式のJSONデータをArticle配列にパースする関数
function parseRSS2(jsonData: any): Article[] {
const items = Array.isArray(jsonData.rss.channel.item)
? jsonData.rss.channel.item
: [jsonData.rss.channel.item];
return items.map((item: any) => parseRSSItem(item));
}
// Atom形式のJSONデータをArticle配列にパースする関数
function parseAtom(jsonData: any): Article[] {
const entries = Array.isArray(jsonData.feed.entry)
? jsonData.feed.entry
: [jsonData.feed.entry];
return entries.map((entry: any) => parseRSSItem(entry, true));
}
// 複数のRSSフィードURLを処理し、最新の10記事を返す関数
export async function fetchAndProcessRSS(
rssURLs: string[]
): Promise<Article[]> {
let articles: Article[] = [];
await Promise.all(
rssURLs.map(async (rssURL) => {
try {
const rssData = await fetchRSS(rssURL); // 各RSSフィードをフェッチ
let fetchedArticles: Article[] = [];
if (rssData.rss) {
fetchedArticles = parseRSS2(rssData); // RSS 2.0形式を解析
} else if (rssData.feed) {
fetchedArticles = parseAtom(rssData); // Atom形式を解析
} else {
console.error(`Unknown feed format for ${rssURL}`, rssData); // 不明なフォーマットのフィード
}
articles.push(...fetchedArticles); // 取得した記事を追加
} catch (error) {
console.error(`Error processing RSS feed ${rssURL}:`, error); // エラー処理
}
})
);
// 記事を日付でソートし、最新の10記事を取得
articles.sort(
(a, b) => new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime()
);
articles = articles.slice(0, 10);
return articles; // 最終的な記事リストを返す
}
src/pages/feed.astro
---
import Layout from "@/layouts/Layout.astro";
import { fetchAndProcessRSS } from "@/libs/fetchAndProcessRSS";
import { Image } from "astro:assets";
import noimage from "@/images/noimage.png";
// RSSフィードのURL 配列で複数RSSフィードURLの指定が可能
const rssURLs = [
"https://note.com/xxx/rss"
];
// RSSフィードから記事を取得して統合
const articles = await fetchAndProcessRSS(rssURLs);
---
<Layout>
<h1>Latest Articles</h1>
<ul>
{
articles.map((article) => (
<li>
<a href={article.link}>
{article.thumb ? (
<img src={article.thumb} alt="" />
) : (
<Image src={noimage.src} width={400} height={300} alt="" />
)}
<div class="info">
<p class="date">{article.pubDate}</p>
<p class="title">{article.title}</p>
</div>
</a>
</li>
))
}
</ul>
</Layout>
😢つまづき⓵XMLデータをJSON形式に変換するパッケージでデプロイ失敗
はじめはxml2jsで試して作成していましたが、いざデプロイしようとしたらエラーでデプロイできませんでした。
エラー内容
02:15:37 [ERROR] [vite] x Build failed in 1.43s
[commonjs--resolver] [plugin vite:resolve] Cannot bundle Node.js built-in "events" imported from "node_modules\xml2js\lib\parser.js". Consider disabling ssr.noExternal
or remove the built-in dependency.
ChatGPTに聞いてみた
このエラーは、Viteビルド中に発生したものです。エラーメッセージによると、
node_modules\xml2js\lib\parser.js
からNode.jsの組み込みモジュールである "events" をインポートしようとしているが、それがバンドルされていないため、エラーが発生しています。エラーの提案として、ssr.noExternal
を無効にするか、組み込みの依存関係を削除することが挙げられています。このエラーの理由として、ViteがデフォルトでNode.jsの組み込みモジュールをバンドルしないためです。これは、Viteがブラウザ向けのフロントエンドアプリケーションを効率的にビルドするための設計上の決定です。しかし、
xml2js
ライブラリがNode.jsの組み込みモジュールである "events" を使用しているため、このエラーが発生しています。
astro.config.mjsのviteに設定を追加したり試したのですが、うまくいきません・・
rss-parserも試してみたのですが、Cannot bundle Node.js built-in "http" imported from "node_modules\rss-parser\lib\parser.js"
と同様のエラーが出たため断念しました。
最終的にはデプロイできたfast-xml-parserで落ち着きました。
😢つまづき②画像を取得するときにundefinedになる
はじめはnoteのRSSフィードの形式をもとに作っていたのですが、複数のRSSフィードからデータを取得するときに画像が取得できないケースに遭遇しました。
<enclosure url="https://xxx.xyz/xxxx.jpg" type="image/jpeg" length="0" />
取得しようと設定してもundefinedになります・・
parserするときに属性が削除されてしまいurlがなくなっていました。そこでXMLParserのignoreAttributesとattributeNamePrefixオプションの設定を追加したら取得できるようになりました🎉
const parser = new XMLParser({
ignoreAttributes: false, // 属性を無視しない
attributeNamePrefix: "", // 属性名のプレフィックスを削除
});
いろんなサービスのRSSフィードを見てみよう👀
※2024/6/11時点
note
<item>
<title>記事タイトル</title>
<media:thumbnail>画像URL</media:thumbnail>
<description><![CDATA[ 本文 ]]></description>
<note:creatorImage>クリエイター画像</note:creatorImage>
<note:creatorName>クリエイター名</note:creatorName>
<pubDate>Thu, 08 Sep 2022 01:47:28 +0900</pubDate>
<link>記事URL</link>
<guid>記事URL</guid>
</item>
Qiita
<entry>
<id>tag:qiita.com,2005:PublicArticle/記事ID?</id>
<published>2024-06-11T15:46:46+09:00</published>
<updated>2024-06-11T15:46:49+09:00</updated>
<link rel="alternate" type="text/html" href="記事URL+パラメータ"/>
<title>記事タイトル</title>
<content type="html">本文</content>
<author>
<name>ユーザ名</name>
</author>
</entry>
Zenn
<item>
<title><![CDATA[ 記事タイトル ]]></title>
<description><![CDATA[ 本文 ]]></description>
<link>記事URL</link>
<guid isPermaLink="true">記事URL</guid>
<pubDate>Wed, 12 Jun 2024 00:00:00 GMT</pubDate>
<enclosure url="OGP画像URL" length="0" type="image/png"/>
<dc:creator>ユーザ名</dc:creator>
</item>
はてなブックマーク
RSS 2.0、Atom形式の両方あります。
<item>
<title>記事タイトル</title>
<link>記事URL</link>
<description>本文</description>
<pubDate>Thu, 23 May 2024 11:57:41 +0900</pubDate>
<guid isPermaLink="false">hatenablog://entry/ID?</guid>
<enclosure url="画像URL" type="image/png" length="0" />
</item>
<entry>
<title>記事タイトル</title>
<link href="記事URL" />
<id>hatenablog://entry/記事ID?</id>
<published>2024-06-11T17:45:54+09:00</published>
<updated>2024-06-11T17:45:54+09:00</updated>
<summary type="html">要約</summary>
<content type="html">本文</content>
<category term="カテゴリ" label="カテゴリ" />
<category term="カテゴリ" label="カテゴリ" />
<link rel="enclosure" href="画像URL" type="image/png" length="0" />
<author>
<name>ユーザ名</name>
</author>
</entry>