「関連記事どうする問題」
自分の書いた記事、全部ちゃんと覚えてる人ってなかなかいなくないですか。少なくとも僕はそう。
そういえば最近年をとって記憶力が単純に衰えてきていて。まあその話は今日はいいです。
なので、関連記事は「同じタグの記事を並べとけばいいか〜」ってなるじゃないですか。でも実際やってみると、タグの粒度がバラバラで関連性が薄かったり、じゃあ「細かくタグを分ければいいじゃん」って思ったら今度はタグが乱立して、1記事しか紐づいてないタグがゴロゴロ……みたいな状況に。
ということで今回は、
- 手動で「この記事とこの記事は関連してる」と指定できるようにする
- 構造や内容が近い記事を自動でおすすめする
この2つを組み合わせて、ちょっと賢い感じの関連記事表示を作ってみました。
実装概要
関連記事用のコンポーネントを新規作成する
対象 : src/components/RelatedPosts.astro
表示中の記事に関連する投稿を抽出・表示するための専用コンポーネント RelatedPosts.astro
を新しく作りました。Notion 側で手動指定する RelatedPostPageIds
、構造的分類を意識した InternalTags
、そしていつもの Tags
を組み合わせて、より納得感のある関連記事を表示できるようにしています。
受け取る props は以下の3つ:
-
currentPost
:表示中の記事 -
allPosts
:全記事一覧 -
maxPosts
:表示する件数(任意、デフォルトは3件)
---
import type { Post, SelectProperty } from '../lib/interfaces.ts';
import { getPostLink, filePath } from '../lib/blog-helpers.ts';
export interface Props {
currentPost: Post;
allPosts: Post[];
maxPosts?: number;
}
const { currentPost, allPosts, maxPosts = 3 } = Astro.props;
// この先に関連記事の抽出ロジックを書いていく
手動指定:RelatedPostPageIds
Notion に relation
プロパティとして RelatedPosts
を追加し、そこから関連記事のIDを取得。こうすることで、記事ごとに「これとこれは関連してるよね」と明示的にリンクできます。
if (currentPost.RelatedPostPageIds && currentPost.RelatedPostPageIds.length > 0) {
relatedPostsToDisplay = currentPost.RelatedPostPageIds.map(pageId =>
allPosts.find(p => p.PageId === pageId)
).filter(p => p !== undefined && p.PageId !== currentPost.PageId) as Post[];
}
🧬 自動推薦:InternalTags / Tags
RelatedPosts
が入力されてなかったり、これだけで表示件数(今回だと3)に満たない場合は、スコアリングで残りを補完します。
InternalTags
Notion の multi_select
を使って、記事を構造的に分類するための内部タグを設定。これは表示には使わず、関連記事抽出用の属性です。一致したタグ1個につきスコア+10してます(今後調整の余地あり)
if (currentPost.InternalTags && currentPost.InternalTags.length > 0) {
allPosts.forEach(p => {
if (p.PageId === currentPost.PageId || relatedPostsToDisplay.some(rp => rp.PageId === p.PageId)) {
return; // 自分自身と既に手動で選ばれた記事は除く
}
if (p.InternalTags && p.InternalTags.length > 0) {
const commonInternalTags = currentPost.InternalTags!.filter(ct =>
(p.InternalTags as SelectProperty[]).some(pt => pt.name === ct.name)
).length;
if (commonInternalTags > 0) {
candidatePosts.push({ ...p, score: commonInternalTags * 10 }); // 内部タグの一致度をスコア化 (例: x10)
}
}
});
}
Tags(表示用タグ)
記事ページに見えてる普通のタグ。こちらの一致は軽めに扱って、1つ一致でスコア +1。これも調整の余地あり(今のロジックだとある意味ない)
if (currentPost.Tags && currentPost.Tags.length > 0) {
allPosts.forEach(p => {
if (p.PageId === currentPost.PageId ||
relatedPostsToDisplay.some(rp => rp.PageId === p.PageId) ||
candidatePosts.some(cp => cp.PageId === p.PageId) // 既に内部タグで候補になっているものは除く
) {
return;
}
if (p.Tags && p.Tags.length > 0) {
const commonDisplayTags = currentPost.Tags.filter(ct =>
p.Tags.some(pt => pt.name === ct.name)
).length;
if (commonDisplayTags > 0) {
candidatePosts.push({ ...p, score: commonDisplayTags }); // 表示タグの一致度をスコア化 (例: x1)
}
}
});
}
記事スコア評価
最後にそれらの中から重複を排除して、関連記事のポストを抽出します。
candidatePosts.sort((a, b) => b.score - a.score);
const finalCandidateIds = new Set(relatedPostsToDisplay.map(p => p.PageId));
for (const scoredPost of candidatePosts) {
if (relatedPostsToDisplay.length >= maxPosts) break;
if (!finalCandidateIds.has(scoredPost.PageId)) {
relatedPostsToDisplay.push(scoredPost);
finalCandidateIds.add(scoredPost.PageId);
}
}
HTML部の表示は各ブログのデザインに合わせて。ザクッとこんな感じ
{relatedPostsToDisplay.length > 0 && (
<div class="related-posts">
<h3>関連記事</h3>
<ul>
{relatedPostsToDisplay.map(post => (
<li><a href={getPostLink(post)}>{post.Title}</a></li>
))}
</ul>
</div>
)}
あとは [slug]
の表示させたい位置に <RelatedPosts currentPost={post} allPosts={allPosts} />
を書けばOKですね。僕は記事の下の方に載せてあります。
まとめ
というわけで、記事を書くときにあまり深く考えなくても自然に関連記事が出せる仕組みをつくってみました。InternalTags
を Notion のAI補完に任せれば、記事を書く → 関連性が生まれる、って流れが自動化できそう。これもやはりNotionでブログを書くことの強みって感じ。
あとはNotion AIでのキーワード生成の精度を上げていくところが課題なので、そこは運用しながら調整ですね。