Next.jsの勉強がてら、SEO対策した簡易的なテンプレートを作ってみました。
全体感
全体感はこんな感じで、記事一覧と記事に紐づくカテゴリが見れます。デザインは簡易的で色々カスタマイズできるようにしています。
フロントはReact/Next.js、ヘッドレスCMSはmicroCMSを使っています。SSGで対応してFirebase Hostingにデプロイできるようにしました。
ざっくり以下の機能を実装しています。
- 記事・カテゴリー機能
- 検索機能
- ヘッダー・フッター
- SSG対応
- ロゴの表示
- サイトマップ自動生成
- 広告の挿入機能
- Google Tag Manager
- お問い合わせフォーム
- プライバシーポリシー
- 運営者情報
- SNSアイコン
- 構造化データ
- パンくずリスト
- メタタグ
- OGP
- 最低限のデザイン・レスポンシブ対応
- 404ページ
- ファビコン
- robot.txt
SEO対策した部分
SSGする
SSGによってビルド時に静的ページを生成し、ホスティングサービスにデプロイすることで、ページ遷移の高速化・レンダリングされたHTMLが生成されているのでクローラーの読み取り可能なため、SEO向上が期待できます。
また、コンテンツにおけるAPIレスポンスは、基本的にビルドのタイミングで全部取得してホスティングしているため、安定的な稼働が可能となります。
next.config.jsにoutput: ‘export’を追加し、next buildすることで静的化されたページが生成されます。
1 2 3 4 5 | module.exports = { // ... output: 'export', // ... } |
構造化データ
構造化データを用いることで、検索エンジンはサイトのコンテンツを認識しやすくなります。
HTMLで構造化されるので、情報の持つ意味がより明確に検索エンジンに伝わり、適切に認識されるようになります。
また、構造化データを用いると、検索結果にリッチスニペットが表示されることがあり、競合のサイトと差別化が図れたり、ユーザーにクリックされやすくなったりします。
例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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) }} /> </> ); }; |
パンくずリスト
パンくずリストを設置することで、ユーザが閲覧しているページが階層構造内のどこに位置しているかを示し、ユーザビリティを高めることができます。
クローラーもパンくずリストによって効率的にそのサイト内のカテゴリーをたどることができるようになるため、サイトの全体像が把握しやすくなり、効率的なクローリングを期待することができます。
例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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> ); } |
サイトマップの自動生成
サイトマップとは、サイト全体のページ構成を地図のように一覧で記載しているページのことです。
ユーザーや検索エンジンにサイト内容をわかりやすく伝える役割を担っています。
サイトマップを用意しておくことで、ユーザが目的のページを探しやすくなったり、検索エンジンがサイト内のページを知らせることができます。
例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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プロパティを使って定義します。
「og-」などのOGPは、FacebookやTwitterをはじめとするSNSとWebサービスの連携を行うために必要なhtmlソースコードへの記述をルール化したものを指します。
例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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, }, }; } |