Next.jsのWebアプリケーションでSEO対策する

Next.jsでWebアプリを作っていく中でSEO対策した部分をまとめてみました。

## SSGする

SSGによってビルド時に静的ページを生成し、ホスティングサービスにデプロイすることで、ページ遷移の高速化・レンダリングされたHTMLが生成されているのでクローラーの読み取り可能なため、SEO向上が期待できます。

Next.js 14からNext.jsnext.config.jsにoutput: 'export'を追加し、next buildすることで静的化されたページが生成されます。

```js
module.exports = {
  // ...
    output: 'export',
  // ...
}
```

パラメーターを受け取る動的なページに関してはgenerateStaticParams()で生成するページを指定します。

例）
```js
export async function generateStaticParams() {
  const articles = await getArticles({limit: 100});

  return articles.contents.map((article) => {
    return { id: article.id }
  })
}
```

## 構造化データ

構造化データを用いることで、検索エンジンはサイトのコンテンツを認識しやすくなります。

HTMLで構造化されるので、情報の持つ意味がより明確に検索エンジンに伝わり、適切に認識されるようになります。

また、構造化データを用いると、検索結果にリッチスニペットが表示されることがあり、競合のサイトと差別化が図れたり、ユーザーにクリックされやすくなったりします。

例）
```js
import { Article } from "@/_libs/microcms";
import Script from "next/script";

export const ArticleJsonLD = ({ article }: { article: Article }) => {
  const url = `${process.env.NEXT_PUBLIC_URL}/articles/${article.id}`;

  const jsonld = [
    {
      "@context": "https://schema.org",
      "@type": "Article",
      mainEntityOfPage: {
        "@type": "WebPage",
        "@id": url,
      },
      headline: article.title,
      datePublished: article.createdAt,
      dateModified: article.updatedAt,
      keywords: "サンプル",
      description: article.description,
      image: article.thumbnail,
      url: `${process.env.NEXT_PUBLIC_URL}/articles/${article.id}`,
    },
  ];

  return (
    <>
      <Script
        id="article-jsonld"
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonld) }}
      />
    </>
  );
};
```

## パンくずリスト

パンくずリストを設置することで、ユーザが閲覧しているページが階層構造内のどこに位置しているかを示し、ユーザビリティを高めることができます。

クローラーもパンくずリストによって効率的にそのサイト内のカテゴリーをたどることができるようになるため、サイトの全体像が把握しやすくなり、効率的なクローリングを期待することができます。

例）
```js
import Link from "next/link";
import { FaChevronRight } from "react-icons/fa";
import styles from './index.module.css';

type Props = {
  lists: {
    title: string,
    path: string
  }[];
};

export default function BreadCrumbs({ lists }: Props) {
  return (
    <ol
      aria-label="breadcrumb"
      className={styles.breadcrumbs}
    >
      {lists.map(({ title, path }, index) => (
        <li key={index}>
          {lists.length - 1 !== index ? (
            <>
              <Link href={path}>
                <span>{title}</span>
              </Link>
              <FaChevronRight aria-hidden="true" />
            </>
          ) : (
            <span aria-current="page">
              {title}
            </span>
          )}
        </li>
      ))}
    </ol>
  );
}
```

## サイトマップの自動生成

サイトマップとは、サイト全体のページ構成を地図のように一覧で記載しているページのことです。

ユーザーや検索エンジンにサイト内容をわかりやすく伝える役割を担っています。

サイトマップを用意しておくことで、ユーザが目的のページを探しやすくなったり、検索エンジンがサイト内のページを知らせることができます。

例）
```js
import { getArticles } from "./_libs/api/article";

const URL = process.env.BASE_URL;

export default async function sitemap() {
  const articles = await getArticles();
  const articlesPages = articles.contents.map((article) => {
    return {
      url: `${URL}/articles/${article.id}`,
      lastModified: new Date(),
    };
  });

  const routes = [
    { url: `${URL}/` },
    { url: `${URL}/articles` },
    { url: `${URL}/company` },
    { url: `${URL}/privacy` },
    { url: `${URL}/contact` },
  ].map((route) => ({
    url: route.url,
    lastModified: new Date(),
  }));

  return [...routes, ...articlesPages, ...categoriesPages, ...tagsPages];
}
```

## メタデータ・OGP

それぞれのページに固有のタイトル、メタディスクリプションを設定します。

canonicalタグは、重複するページが存在する場合、どのURLが1番重要であるかを検索エンジンに対して指定するHTMLタグのことです。alternatesプロパティを使って定義します。

openGraphなどのOGPは、FacebookやXなどをはじめとするSNSとWebサービスの連携を行うために必要なhtmlソースコードへの記述をルール化したものを指します。SNSなどにリンクを貼ると画像やタイトルが表示される設定です。

例）
```js
export const generateMetadata = async ({ params }: Props): Promise<Metadata> => {
  const article = await getArticleById(params.id);

  return {
    title: article.title,
    description: article.description,
    alternates: {
      canonical: `${process.env.BASE_URL}/articles/${params.id}`
    },
    openGraph: {
      type: "website",
      siteName: process.env.NEXT_PUBLIC_TITLE,
      title: article.title,
      description: article.description,
      images: [article.thumbnail.url],
    },
  };
}
```