こんにちは!microCMSでカスタマーエンジニアをしている高宮です。
microCMSを使った関連記事のレコメンド機能の実装方法についてご紹介します。
日々テクニカルサポートを通して、よくお問い合わせいただくのがどのようにレコメンド機能を実装すればよいかという質問です。
「レコメンド機能」といっても、その実装方法は目的やニーズに応じて多様です。そこで、今回は以下の3つの方法についてAPI設計やフロントエンドの実装方法をご紹介します。
- 手動でコンテンツを選択して表示する方法
- 同カテゴリのコンテンツを自動で表示する方法
- キーワードを設定して類似のコンテンツを表示する方法
はじめに
今回ご紹介する3つの方法はそれぞれ異なる方法でコンテンツを取得しますが、すべて共通のビューで表示します。
まずは共通のコンポーネントの実装を確認します。以下のコードでは、取得したコンテンツを表示するためのRecommend
コンポーネントを定義しています。
今回はNext.jsとTailwind CSSを使用していますが、他のフレームワークでも同様の実装が可能ですのでご自身の環境に合わせて実装してみてください。
// /app/_components/recommend/index.tsx
import type { Blog } from '@/app/_types'
import type { Route } from 'next'
import Image from 'next/image'
import Link from 'next/link'
type Props = {
contents: Blog[]
title: string
}
export default function Recommend({ contents, title }: Props) {
if (!contents || contents.length === 0) {
return <div>記事がありません</div>
}
return (
<section className='space-y-8'>
<div className='rounded-lg bg-white p-4 shadow-md'>
<h3 className='mb-4 text-lg font-semibold'>{title}</h3>
<div className='grid gap-2'>
{contents.map(item => (
<div key={item.id} className='grid gap-4'>
<Link href={`/${item.id}` as Route} className='group flex gap-4'>
<Image
src={item.eyecatch.url}
alt={item.title}
width={item.eyecatch.width}
height={item.eyecatch.height}
className='aspect-square w-20 rounded-lg object-cover transition-opacity group-hover:opacity-80'
/>
<div className='grid gap-1'>
<h4 className='text-base transition-colors group-hover:text-primary'>
{item.title}
</h4>
</div>
</Link>
</div>
))}
</div>
</div>
</section>
)
}
このコンポーネントは、取得したコンテンツとタイトルをPropsで受け取り表示しています。
1. 手動でコンテンツを選択して表示する方法
優先的に表示したいコンテンツを手動で設定する場合は、複数コンテンツ参照フィールドを利用する方法があります。
利用イメージ
例えば、トップページやブログのサイドバーに「おすすめコンテンツ」や「ピックアップ」のように表示したい場合、管理画面から複数のコンテンツを手動で選んで設定できます。
APIスキーマ設計
以下のようにAPIスキーマを設計します。ブログ(リスト形式)
- title:タイトル(テキストフィールド)
- description:説明(テキストエリア)
- eyecatch:サムネイル画像(画像)
- content:本文(リッチエディタ)
- pickup:ブログ (複数コンテンツ参照 - ブログAPIを参照)
pickup
では複数コンテンツ参照を設定してブログAPIを参照しています。
選択してリクエストすると選択したブログの情報を含んだ形で返却されます。
{
"id": "5uvjill89v4l",
"title": "記事のタイトル1",
"description": "この記事の概要です。",
"content": "<p>本文が入ります</p>",
"eyecatch": {
"url": "https://images.microcms-assets.io/assets/8a796dcc189a4808b14b21e3946bd016/1efef24a3bb24f5a8612e97be74f83bb/16%3A9.jpeg",
"height": 675,
"width": 1200
},
"pickup": [
{
"id": "dam6r1h1boan",
"title": "記事のタイトル5",
"eyecatch": {
"url": "https://images.microcms-assets.io/assets/8a796dcc189a4808b14b21e3946bd016/1efef24a3bb24f5a8612e97be74f83bb/16%3A9.jpeg",
"height": 675,
"width": 1200
}
},
{
"id": "c31qxxtlzjjk",
"title": "記事のタイトル4",
"eyecatch": {
"url": "https://images.microcms-assets.io/assets/8a796dcc189a4808b14b21e3946bd016/1efef24a3bb24f5a8612e97be74f83bb/16%3A9.jpeg",
"height": 675,
"width": 1200
}
},
{
"id": "oicwuew_tfz",
"title": "記事のタイトル2",
"eyecatch": {
"url": "https://images.microcms-assets.io/assets/8a796dcc189a4808b14b21e3946bd016/1efef24a3bb24f5a8612e97be74f83bb/16%3A9.jpeg",
"height": 675,
"width": 1200
}
}
]
}
このpickup
の値を利用して、コンテンツを表示するように実装します。
フロントエンドの実装
次に、ピックアップで選択したブログを取得し、サイドバーに表示するためのコンポーネントを作成します。
// /app/[slug]/index.tsx
// …中略
const getBlogDetail = async (contentId: string) => {
const blog = await client.getListDetail<Blog>({
contentId,
endpoint: 'blogs',
})
return blog
}
const getBlogsByCategory = async (queries?: MicroCMSQueries) => {
const blogList = await client.getList<Blog>({
endpoint: 'blogs',
queries,
})
return blogList
}
export default async function Page({
params: { slug },
}: {
params: { slug: string } // URLからアクセスしているブログのslugを取得
}) {
// ブログの詳細情報を取得
const blog = await getBlogDetail(slug)
if (!blog) {
return notFound()
}
// 手動で設定したコンテンツを取得
const { pickup } = blog
return (
<main className='grid gap-8 md:grid-cols-[1fr_360px]'>
{/* ブログの詳細情報を表示するコンポーネント */}
<BlogDetail blog={blog} />
{/* 手動で選択したブログを表示するコンポーネント */}
<Recommend contents={pickup} title='ピックアップ' />
</main>
)
}
処理の流れとしては以下のようになります。
- アクセスしているURLからslugを取得
- getBlogDetail関数でブログの詳細情報を取得
- 2.で取得したブログの詳細情報から手動で設定したコンテンツを取得
- 取得したブログ詳細と手動で設定したコンテンツをそれぞれのコンポーネントに渡す
メリット・デメリット
この方法のメリットとデメリットは以下の通りです。
メリット
- 意図した通りにコンテンツを表示できる
- 特定の記事や時期に応じて、柔軟に調整できる
デメリット
- 記事が増えるたびに手動で関連付けを行う必要があり、運用時の負担が大きい
2. 同カテゴリのコンテンツを自動で表示する方法
現在アクセスしているコンテンツと同じカテゴリに属するコンテンツを自動で表示する方法もあります。
利用イメージ
例えば、ブログに「テクノロジー」というカテゴリが設定されている場合、サイドバーには、同じ「テクノロジー」カテゴリに属する他のコンテンツが自動で表示されます。
APIスキーマ設計
以下のようにAPIスキーマを設計します。カテゴリ(リスト形式)
- name:カテゴリ名(テキストフィールド)
ブログ(リスト形式)
- title:タイトル(テキストフィールド)
- description:説明(テキストエリア)
- eyecatch:サムネイル画像(画像)
- content:本文(リッチエディタ)
- category:カテゴリ(コンテンツ参照 - カテゴリAPIを参照)
このように設定することでブログの編集画面からカテゴリを選択できるようになります。
カテゴリを選択してリクエストすると、カテゴリAPIのレスポンスを含んだ形で返却されます。
{
"id": "339xo8i09",
"createdAt": "2024-09-04T23:22:50.426Z",
"updatedAt": "2024-09-09T08:59:00.239Z",
"publishedAt": "2024-09-04T23:22:50.426Z",
"revisedAt": "2024-09-09T08:59:00.239Z",
"title": "記事のタイトル",
"description": "この記事の概要です。",
"content": "<p>本文が入ります。</p>",
"eyecatch": {
"url": "https://images.microcms-assets.io/assets/2de8aaa5dbd44c939b1aa6dc62a4f095/1efef24a3bb24f5a8612e97be74f83bb/16%3A9.jpeg",
"height": 675,
"width": 1200
},
"category": {
"id": "do2ys5g66tq",
"createdAt": "2024-08-30T06:56:10.826Z",
"updatedAt": "2024-08-30T06:56:10.826Z",
"publishedAt": "2024-08-30T06:56:10.826Z",
"revisedAt": "2024-08-30T06:56:10.826Z",
"name": "テクノロジー"
}
}
このカテゴリAPIのレスポンスを利用して、同じカテゴリのコンテンツを取得する処理を実装します。
フロントエンド実装
// /app/[slug]/index.tsx
// …中略
const getBlogDetail = async (contentId: string) => {
const blog = await client.getListDetail<Blog>({
contentId,
endpoint: 'blogs',
})
return blog
}
const getBlogsByCategory = async (queries?: MicroCMSQueries) => {
const blogList = await client.getList<Blog>({
endpoint: 'blogs',
queries,
})
return blogList
}
export default async function Page({
params: { slug },
}: {
params: { slug: string } // URLからアクセスしているブログのslugを取得
}) {
// ブログの詳細情報を取得
const blog = await getBlogDetail(slug)
if (!blog) {
return notFound()
}
// カテゴリのコンテンツIDを取得
const { id: categoryId } = blog.category
// filtersパラメータで同じカテゴリのブログを取得
const { contents: relatedContents } = await getBlogsByCategory({
limit: 3,
filters: `category[equals]${categoryId}[and]id[not_equals]${slug}`,
})
return (
<main className='grid gap-8 md:grid-cols-[1fr_360px]'>
{/* ブログの詳細情報を表示するコンポーネント */}
<BlogDetail blog={blog} />
{/* 関連記事を表示するコンポーネント */}
<Recommend contents={relatedContents} title='関連記事' />
</main>
)
}
処理の流れとしては以下のようになります。
- アクセスしているURLからslugを取得
- getBlogDetail関数でブログの詳細情報を取得
- 2.で取得したブログの詳細情報からカテゴリのコンテンツIDを取得
- getBlogsByCategory関数で、同じカテゴリに属するコンテンツを絞り込んで取得
- 取得したブログ詳細と関連記事をそれぞれのコンポーネントに渡す
getBlogsByCategory関数の内容
getBlogsByCategory
関数では、同じカテゴリに属するコンテンツを取得するため、filtersパラメータを使用しています。
ここでは、特定のカテゴリに属しているコンテンツのみを取得し、さらに現在表示中のコンテンツは除外するという2つのフィルタリング条件を適用しています。
filters: category[equals]${categoryId}: アクセスしているコンテンツと同じカテゴリのコンテンツを絞り込みます。
filters: id[not_equals]${slug}:現在表示中のコンテンツを結果から除外します。
このフィルタリングにより、アクセスしているコンテンツと同じカテゴリに属する、他の3つのコンテンツが表示される仕組みになっています。
メリット・デメリット
この方法のメリットとデメリットは以下の通りです。
メリット
- カテゴリを選択すれば自動で関連性の高いコンテンツを表示できる
- コンテンツ作成時には、すでに設定してあるカテゴリを選択するだけなので、コンテンツ数や運用メンバーが増えても運用コストが低い
デメリット
- カテゴリの範囲が広すぎる場合、関連性の低いコンテンツが表示される可能性がある
- APIを2つ設定する必要がある
3. キーワードを設定して類似のコンテンツを表示する方法
コンテンツにキーワードを設定し、それに基づいて類似するコンテンツを表示する方法について説明します。
利用イメージ
例えば、コンテンツに「Next.js」「React」「JavaScript」などのキーワードを設定します。これらのキーワードを含む他のコンテンツが自動的に関連コンテンツとして表示されます。
APIスキーマ設計
ブログ(リスト形式)
- title:タイトル(テキストフィールド)
- description:説明(テキストエリア)
- eyecatch:サムネイル画像(画像)
- content:本文(リッチエディタ)
- keyword:キーワード(テキストフィールド)
このように設定し、ブログの編集画面からキーワードを設定します。
キーワードを設定してリクエストをすると以下のような値が返却されます。
{
"id": "5uvjill89v4l",
"createdAt": "2024-09-02T22:49:37.008Z",
"updatedAt": "2024-09-11T22:58:12.126Z",
"publishedAt": "2024-09-02T22:49:37.008Z",
"revisedAt": "2024-09-11T22:58:12.126Z",
"title": "記事のタイトル1",
"description": "この記事の概要です。",
"content": "<p>本文が入ります。</p>",
"eyecatch": {
"url": "https://images.microcms-assets.io/assets/8a796dcc189a4808b14b21e3946bd016/1efef24a3bb24f5a8612e97be74f83bb/16%3A9.jpeg",
"height": 675,
"width": 1200
},
"keyword": "Next.js"
}
このkeyword
の値を利用して、類似コンテンツを表示するように実装します。
フロントエンドの実装
キーワードを使って類似コンテンツを表示する際、qパラメータを利用する方法があります。
// /app/[slug]/index.tsx
// …中略
const getBlogDetail = async (contentId: string) => {
const blog = await client.getListDetail<Blog>({
contentId,
endpoint: 'blogs',
})
return blog
}
const getBlogsByKeyword = async (queries?: MicroCMSQueries) => {
const blogList = await client.getList<Blog>({
endpoint: 'blogs',
queries,
})
return blogList
}
export default async function Page({
params: { slug },
}: {
params: { slug: string } // URLからアクセスしているブログのslugを取得
}) {
// ブログの詳細情報を取得
const blog = await getBlogDetail(slug)
if (!blog) {
return notFound()
}
// キーワードを取得
const { keyword } = blog
// qパラメータで全フィールドを対象にキーワード検索
const { contents: relatedContents } = await getBlogsByKeyword({
limit: 3,
q: keyword,
filters: `id[not_equals]${slug}`,
})
return (
<main className='grid gap-8 md:grid-cols-[1fr_360px]'>
{/* ブログの詳細情報を表示するコンポーネント */}
<BlogDetail blog={blog} />
{/* 関連記事を表示するコンポーネント */}
<Recommend contents={relatedContents} title='関連記事' />
</main>
)
}
処理の流れとしては以下のようになります。
- アクセスしているURLからslugを取得
- getBlogDetail関数でブログの詳細情報を取得
- 2.で取得したブログの詳細情報からキーワードを取得
- getBlogsByKeyword関数で、qパラメータを使ってコンテンツを絞り込む
- 取得したブログ詳細と関連記事をそれぞれのコンポーネントに渡す
getBlogsByKeyword関数の内容
getBlogsByKeyword
関数では、類似コンテンツを取得するためにqパラメータを使用しています。
qパラメータは、コンテンツの全文検索ができるパラメータです。また、特定のフィールド(例えばタイトルや本文)のみを対象にするのではなく、複数のフィールドにまたがって検索が行われます。
q: keyword: keywordの値で全文検索を行い、類似のコンテンツを取得します。
filters: id[not_equals]${slug}:現在表示中のコンテンツを結果から除外します。
このフィルタリングにより、アクセスしているコンテンツと類似のコンテンツが3つ表示される仕組みになっています。
メリット・デメリット
この方法のメリットとデメリットは以下の通りです。
メリット
- ordersパラメータで検索結果のソート順を指定しない場合、キーワードの合致度が高いコンテンツ順に自動で表示できる
- 記事の本文部分も検索対象になるため、入稿者が認識しづらい関連コンテンツについても自動で表示できる
デメリット
- 形態素解析の仕様上、コンテンツにキーワードと一致するテキストが登録されていても単語の分割位置によっては表示されないことがある
- 他のコンテンツの編集に応じてキーワードの合致度も変わるため、表示されるコンテンツや順番も変わる可能性がある
さいごに
今回はmicroCMSでレコメンド機能を実装する方法について紹介しました!ぜひ参考にしてみてください!
なお、アプリケーションのリポジトリは公開していますので、興味のある方はぜひ見てみてください。
https://github.com/wasabi-tr/microcms-post-recommend