カテゴリ・タグ一覧ページのSEO対策を行う(Next.js)

PRを含みます

課題

categoryやtagのコンポーネントを作って、一覧ページも作成していたが、titleがすべて新着記事一覧というblogのtitleを参照していた。descriptionもblogのを参照している。カテゴリページやタグページにGoogleから直接アクセスすることはあまりないと思うけども、同じタイトルのページが多数存在することはちょっと気持ち悪いので、今回対策を行う。

修正後(categoryコンポーネントの例)

import {
  getCategoryDetail,
  getNewsList,
  getAllCategoryList,
} from "@/app/_libs/microcms";
import { notFound } from "next/navigation";
import { Metadata } from "next"; // Metadata型をインポート
import NewsList from "@/app/_components/NewsList";
import Pagenation from "@/app/_components/Pagenation";
import Category from "@/app/_components/Category";
import { NEWS_LIST_LIMIT } from "@/app/_constants";

export async function generateStaticParams() {
  const categories = await getAllCategoryList();

  return categories.map((category: { id: string }) => ({
    id: category.id,
  }));
}

type Props = {
  params: Promise<{
    id: string;
  }>;
};

// --- ここから追加 ---
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const category = await getCategoryDetail(id).catch(() => null);

  if (!category) {
    return {
      title: "カテゴリ未設定 | monaka",
    };
  }

  return {
    title: `${category.name}の記事一覧 | monaka`,
    description: `${category.name}に関する新着記事一覧です。`,
  };
}
// --- ここまで追加 ---

export default async function Page({ params }: Props) {
  const { id } = await params;

  const category = await getCategoryDetail(id).catch(notFound);

  const { contents: news, totalCount } = await getNewsList({
    limit: NEWS_LIST_LIMIT,
    filters: `category[equals]${category.id}`,
  });

  return (
    <>
      <p>
        <Category category={category} />
        の一覧
      </p>
      <NewsList news={news} />
      <Pagenation
        totalCount={totalCount}
        basePath={`/blog/category/${category.id}`}
      />
    </>
  );
}

表示イメージ

  • Web制作の記事一覧 (2ページ目) | monaka

のように表示されるようにした。もし参考にする方がいればブログのタイトル周りなどそのあたりは適宜読み替えをしてほしい。

ページネーションも対応する

category配下にpというディレクトリがある。そこもタイトルがすべて同じになってしまっていたので対策する。最終的に以下のようにした

import { notFound } from "next/navigation";
import { Metadata } from "next";
import {
  getCategoryDetail,
  getNewsList,
  getAllCategoryList,
} from "@/app/_libs/microcms";
import NewsList from "@/app/_components/NewsList";
import Pagenation from "@/app/_components/Pagenation";
import { NEWS_LIST_LIMIT } from "@/app/_constants";

type Props = {
  params: Promise<{
    id: string;
    current: string;
  }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id, current: currentStr } = await params;
  const current = parseInt(currentStr, 10);

  const category = await getCategoryDetail(id).catch(() => null);

  if (!category) {
    return { title: "カテゴリー | monaka" };
  }

  // 1ページ目の時は表記なし、2ページ目以降は「(nページ目)」と表示
  const pageSuffix = current > 1 ? ` (${current}ページ目)` : "";

  return {
    title: `${category.name}の記事一覧${pageSuffix} | monaka`,
    description: `${category.name}に関する新着記事一覧の${current}ページ目です。`,
  };
}

export async function generateStaticParams() {
  const categories = await getAllCategoryList();
  const paths = [];

  for (const category of categories) {
    const { totalCount } = await getNewsList({
      limit: 0,
      filters: `category[equals]${category.id}`,
    });

    const maxPage = Math.ceil(totalCount / NEWS_LIST_LIMIT);

    for (let p = 1; p <= maxPage; p++) {
      paths.push({
        id: category.id,
        current: p.toString(),
      });
    }
  }

  return paths;
}

export default async function Page({ params }: Props) {
  const { id, current: currentStr } = await params;

  const current = parseInt(currentStr, 10);
  if (Number.isNaN(current) || current < 1) {
    notFound();
  }

  const category = await getCategoryDetail(id).catch(notFound);

  const { contents: news, totalCount } = await getNewsList({
    filters: `category[equals]${category.id}`,
    limit: NEWS_LIST_LIMIT,
    offset: NEWS_LIST_LIMIT * (current - 1),
  });

  if (news.length === 0) {
    notFound();
  }

  return (
    <>
      <NewsList news={news} />
      <Pagenation
        totalCount={totalCount}
        current={current}
        basePath={`/blog/category/${category.id}`}
      />
    </>
  );
}

まとめ

今回はtitle、descripionの修正を行った。これでタイトルの重複がなくなったので少しはマシになったかと思う。最近は生成AIの利用が増えてきたことに伴ってオーガニック検索からの流入が減少傾向にあると聞くけれども、まだまだGoogleからの流入もあるはずなのでしっかりSEO対策も行っていきたいと思う。

Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門 高速で、安全で、運用しやすいサイトのつくりかた
Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門 高速で、安全で、運用しやすいサイトのつくりかた
この記事を書いた人
もなか

もなか

Web制作で学んだことApple、スマホ関連のニュース、日常のつぶやきをまとめています。お気軽にフォローしてください。

関連記事