Astro + microCMSのブログにRSSフィードを実装した
Astro + microCMS + Cloudflare PagesのブログにRSSフィードを実装した。
公式パッケージ@astrojs/rssで/rss.xmlを配信しつつ、フッターやAboutの「RSS」リンクからは/rss/というHTMLの説明ページに案内する二段構えにしてみた。
子供の闘病ブログは親世代の訪問が多いので、RSSリーダーの説明を入れるとより親切かなと考えた。
公開URL:
構成
RSSリーダー ─────→ /rss.xml (XML、本物のフィード)
▲
│ <link rel="alternate"> で autodiscovery
│
人間の読者 ─────→ /rss/ (HTML、説明とURLコピー導線)
↑ ↓
フッター/About URLコピー + リーダー紹介機械(RSSリーダー)は/rss.xmlを直接読みに行く。人間の読者がフッターやAboutから踏んだときは、まずHTMLの説明ページ/rss/を経由する。
/rss.xmlの配信
公式パッケージを使う。
npm install @astrojs/rsssrc/pages/rss.xml.tsを作成する。
import rss from '@astrojs/rss';
import type { APIContext } from 'astro';
import { getArticles, articlePath, extractExcerpt } from '../lib/microcms';
import { SITE } from '../consts';
export async function GET(context: APIContext) {
const { totalCount } = await getArticles({ limit: 1 });
const items =
totalCount === 0
? []
: (
await getArticles({ limit: totalCount, orders: '-day' })
).contents.map((article) => ({
title: article.title,
pubDate: new Date(article.day),
description: extractExcerpt(article.body, 200),
content: article.body,
link: articlePath(article),
}));
return rss({
title: SITE.title,
description: SITE.description,
site: context.site ?? SITE.url,
items,
xmlns: { content: 'http://purl.org/rss/1.0/modules/content/' },
customData: '<language>ja-JP</language>',
});
}microCMSのlimit挙動
microCMSのリストAPIはlimitがデフォルト10、引数で指定しても最大100まで。全件取りたいときはまずlimit: 1でtotalCountを取得し、その値をlimitに渡している。
ソートはorders: '-day'で記録日の新しい順。
descriptionとcontent:encodedを両方出す
@astrojs/rssのitemに渡せるフィールドのうち、本文を載せられるのは以下の2つ。
description: 抜粋。HTMLタグをstripして200字に切っているcontent: 本文HTMLそのまま。出力上はcontent:encoded要素になる
両方含めると、フィードリーダー上で抜粋一覧と全文表示の両方ができる。
content:encodedのためのxmlns宣言
content:encodedはRSS 2.0コア仕様ではなく拡張仕様で、namespace宣言が必要になる。@astrojs/rssではxmlnsオプションで渡す。
xmlns: { content: 'http://purl.org/rss/1.0/modules/content/' },pubDateにはdayを渡す
microCMSはシステムフィールドpublishedAt(記事の公開日時)を持っているが、このブログでは記録日を表すカスタムフィールドdayを別に持っている。(⇦闘病ブログという性質のため記録日を大切にしたかったので。)pubDateにはpublishedAtではなくdayを渡している。
pubDate: new Date(article.day),autodiscoveryのリンクをheadに追加
RSSリーダー(Feedly、Inoreader等)はサイトのトップURLから<link rel="alternate">をたどってフィードを検出する。BaseLayout.astroの<head>に1行追加する。
<link
rel="alternate"
type="application/rss+xml"
title={SITE.title}
href="/rss.xml"
/>これでhttps://west-blog.pages.dev/をリーダーに登録するだけで/rss.xmlが検出される。
フッター/AboutにRSSリンクを置く
Footer.astroに「RSS」リンクを置く。リンク先は/rss.xmlではなく次に作る/rss/。
<a href="/rss/" aria-label="RSSフィードを購読">
<svg viewBox="0 0 24 24" width="12" height="12" aria-hidden="true">
<circle cx="5.5" cy="18.5" r="2.5" />
<path d="M3 11v4c4.97 0 9 4.03 9 9h4c0-7.18-5.82-13-13-13z" />
<path d="M3 3v4c9.39 0 17 7.61 17 17h4C24 12.85 14.85 3 3 3z" />
</svg>
<span>RSS</span>
</a>Aboutページにも「新しい記録を受け取る」セクションを追加してみた。
/rss/着地ページ
src/pages/rss.astroを作成する。中身は以下。
- RSSアイコン(56px)
- フィードURLの表示とコピーボタン
- RSSリーダーの説明
- 推奨リーダー(Feedly / Inoreader / NetNewsWire)
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { SITE } from '../consts';
const feedUrl = new URL('/rss.xml', SITE.url).toString();
---
<BaseLayout title="RSSフィードを受け取る">
<article>
<header>
<p class="eyebrow">RSS</p>
<h1>新しい記録を受け取る</h1>
<svg viewBox="0 0 24 24" width="56" height="56" aria-hidden="true">
<circle cx="5.5" cy="18.5" r="2.5" />
<path d="M3 11v4c4.97 0 9 4.03 9 9h4c0-7.18-5.82-13-13-13z" />
<path d="M3 3v4c9.39 0 17 7.61 17 17h4C24 12.85 14.85 3 3 3z" />
</svg>
</header>
<section>
<p>フィードURL</p>
<div>
<code id="rss-feed-url">{feedUrl}</code>
<button type="button" data-target="rss-feed-url">
<span class="default">URLをコピー</span>
<span class="done" aria-hidden="true">コピーしました</span>
</button>
</div>
</section>
<section>
<h2>RSSリーダーって?</h2>
<ul>
<li><strong>Feedly</strong> — Web / iOS / Android</li>
<li><strong>Inoreader</strong> — Web / iOS / Android</li>
<li><strong>NetNewsWire</strong> — Mac / iPhone</li>
</ul>
</section>
</article>
</BaseLayout>コピーボタンのスクリプト
<script>
const buttons = document.querySelectorAll('.rss-page__copy');
buttons.forEach((btn) => {
btn.addEventListener('click', async () => {
const targetId = btn.dataset.target;
const el = document.getElementById(targetId);
const text = (el.textContent ?? '').trim();
try {
await navigator.clipboard.writeText(text);
btn.classList.add('is-done');
setTimeout(() => btn.classList.remove('is-done'), 2000);
} catch {
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
});
});
</script>navigator.clipboardはsecure context(HTTPS、またはlocalhost)でのみ使える。HTTPでアクセスしたローカル、cross-originのiframe等の環境ではrejectされるので、catchでテキスト範囲を選択状態にするフォールバックを入れている。
参考リンク: