Editorial newsの実装メモ — Astro 5で組んだ8つの仕組み
LeapLensのお知らせ (news) は、editorialメディアとして必要な機能を一通り備えている。 個別の機能はライブラリで足せても、それぞれの設計判断こそが資産になる。 この記事は、8つの主要な仕組みについて、なぜそれを選んだかをまとめておく。
1. 検索 (Pagefind)
ビルド時に dist/news/**/*.html をスキャンし、検索インデックスを dist/pagefind/ に出力する。
JSは約60KB(gz)で、検索結果のリンクは即時表示される。
<article data-pagefind-body> をお知らせ詳細ページに付与し、関連記事 / 共有 / navが結果に紛れない。
2. 関連記事
src/lib/news.ts の getRelatedPosts(posts, currentId, limit) で実装。
const scored = posts
.filter((p) => p.id !== currentId)
.map((p) => ({ post: p, score: p.data.tags.filter((t) => currentTags.has(t)).length }))
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score || newest);
共通タグ数でスコアし、同点なら新着順。タグが無い記事は空配列。
3. 記事内TOC
render(entry) が返す headings から depth 2-3 を拾って自動生成。#${slug} でアンカー移動。
4. prev / nextナビ
getAdjacentPosts(posts, currentId) で「ひとつ前/ひとつ次」を返す。最古 / 最新で片側null。
noUncheckedIndexedAccess を有効化しているので posts[i ± 1] ?? null でundefinedを排除。
5. 共有ボタン
X (post intent) / Hatena (entry/panel) / Copy Linkの3つ。Copyは navigator.clipboard.writeText() で1.5秒 “Copied!” 表示。
6. 読了時間
CJKと英単語で重み付けした独自計算: CJK 600字/分、英1単語 ≒ 2.4 unit。 混在文書でも自然な分数になる。
7. 構造化データ
ページごとに以下を出力:
- Organization + WebSite (BaseLayoutがデフォルトで)
- Article (keywords + wordCount)
- BreadcrumbList
- ItemList (一覧 / タグページ)
加えて og:type=article + article:published_time / modified_time / section / tag のOG article namespace、Microformats h-entryも併用。
8. 動的OG画像
astro-og-canvas でsite / news / lps / tagsの4系統 にPNG生成。
フォントはInter (英数) + Noto Sans JP (CJK) のスタック。
キャッシュは node_modules/.cache/og-images に置き、再ビルドのcold→warmが約700ms短縮される。
これらは個別には小さい仕組みだが、組み合わさると「読み手にとってのeditorial体験」になる。 1つでも欠けると違和感が出るので、初期に詰め込んだ判断は今のところ正解だったと思う。
なお、i18n (ja/en) は当初検討したが、辞書設計の運用コストとフラッグシップが日本語ターゲットである現状を鑑みて撤回した。背景は別記事「i18n辞書を1ファイルに集約する設計判断」を参照。