Next.js - チュートリアル後

1. Next.js のチュートリアルを完了すると

Next.js のチュートリアルを最後までやるとブログの記事一覧ページと記事ページ(マークダウン)を作成することができます。これだけだとブログとしては物足りない気がするので以下の機能を作成してみたので書いておこうと思います。あまり参考がなかったので良い書き方かはわかりませんがとりあえず機能するかと思います。

  • ページネーション
  • 前後の記事へのリンク
  • カテゴリー/タグページ
  • SEO

2. ページネーションの作成

Next.js は参考が少ないのですが、ページネーションはGOTOHAYATO さんの記事を参考にさせていただきました。はじめに記事一覧のページ分割を動的に作成するために、ここでは「pages > archive > [page].js」というファイルを作成します。作成した[page].jsに下記を記述します。getStaticPaths()でページのパス(URL)を作成しgetStaticProps()で記事にデータを渡します。

//[page].js

const postsPerPage = 2 //記事一覧に表示する記事数

export async function getStaticPaths() {
  const totalPosts = getSortedPostsData().length //記事総数
  const numPages = Math.ceil(totalPosts / postsPerPage) //記事総数 ÷ 表示する記事数
  const numArr = Array.from({ length: numPages }, (_, i) => `${i + 1}`)
  const paths = numArr.map(page => ({
    params: { page: page },
  }))
  return {
    paths: paths,
    fallback: false,
  }
}

export async function getStaticProps({ params }) {
  const allPosts = getSortedPostsData(params.page)
  const page = parseInt(params.page, 10) //現在のページ
  const end = postsPerPage * page //表示する記事数 × 現在のページ
  const start = end - postsPerPage //(表示する記事数 × 現在のページ) - 表示する記事数
  const totalPosts = getSortedPostsData().length
  const numPages = Math.ceil(totalPosts / postsPerPage)

  return {
    props: {
      allPosts: allPosts.slice(start, end),
      numPages,
    },
  }
}

Math.ceil() - 整数を返す
Array.from() - Array インスタンスを生成
parseInt() - 指定された基数の整数値を返す
Array.prototype.slice() - 配列の浅いコピーを新しい配列オブジェクトに作成して返す

ページネーションを出力

getStaticProps()で渡したデータをもとにページネーションを出力します。

<ul>
  {Array.from({ length: numPages }, (_, i) => (
    <li key={`pagination-number${i + 1}`}>
      <Link href={`/archive/${i + 1}`}>
        <a>{i + 1}</a>
      </Link>
    </li>
  ))}
</ul>

3. 前後記事へのリンクを作成

前後記事へのリンクは、チュートリアルで作成する「pages > posts > [id].js」に追記していきます。ここでは、getSortedPostsData()を使うのでimportして下記を追記します。

export async function getStaticProps({ params }) {
  const post = await getPostData(params.id)
  const allPosts = getSortedPostsData()
  const currentPost = allPosts.find(data => data.id === params.id) //現在のページ
  const postNum = allPosts.indexOf(currentPost) //現在のページ番号
  const prevPost =
    postNum === allPosts.length - 1 ? null : allPosts[postNum + 1]
  const nextPost = postNum === 0 ? null : allPosts[postNum - 1]

  return {
    props: {
      post,
      prevPost,
      nextPost,
    },
  }
}

Array.prototype.indexOf() - 配列要素の添字を返す

前後記事へのリンクを出力

前後記事へのリンクは下記のようにすると出力できます。もし前後記事のタイトルを出力したい場合は、{prevPost.title}で出力できます。

<ul>
  {prevPost && (
    <li>
      <Link href={`/posts/${prevPost.id}`}>
        <a>&laquo; Prev</a>
      </Link>
    </li>
  )}
  {nextPost && (
    <li>
      <Link href={`/posts/${nextPost.id}`}>
        <a>Next &raquo;</a>
      </Link>
    </li>
  )}
</ul>

4. カテゴリー/タグページの作成

ここではカテゴリーは記事にひとつ。タグは記事に複数つけられるように設定します。

カテゴリーページ

まずカテゴリーページを作成します。[category].jsを作成し下記を記述します。filter()を使用してカテゴリーと一致した記事を出力します。

export async function getStaticPaths() {
  const allPosts = getSortedPostsData()
  const paths = allPosts.map(categoryName => ({
    params: { category: categoryName.category },
  }))

  return {
    paths,
    fallback: false,
  }
}

export async function getStaticProps({ params }) {
  const allPosts = getSortedPostsData(params.category)
  const currentCat = params.category
  const categoryPosts = allPosts.filter(data => data.category == currentCat)

  return {
    props: {
      currentCat,
      categoryPosts,
    },
  }
}

タグページ

タグは配列の中に配列という構造になってしまうので、少し工夫しなければいけませんがやっていることはカテゴリーページを作成するのと同じです。[tags].jsを作成し下記を記述します。getStaticPaths()でカテゴリーと違う部分は、一度タグの配列をまとめてから重複したものを削除し出力しています。getStaticProps()では、そのままだとfilter()が効かないのでsome()メソッドを使用します。これはevery()メソッドでも大丈夫だと思います。

export async function getStaticPaths() {
  const allPosts = getSortedPostsData()
  const tagsFlat = allPosts.flatMap(data => data.tags)
  const tagsGroup = Array.from(new Set(tagsFlat))
  const paths = tagsGroup.map(tagsName => ({
    params: { tags: tagsName },
  }))

  return {
    paths,
    fallback: false,
  }
}

export async function getStaticProps({ params }) {
  const allPosts = getSortedPostsData(params.tags)
  const currentTag = params.tags
  const tagsPosts = allPosts.filter(data =>
    data.tags.some(x => x == currentTag)
  )

  return {
    props: {
      currentTag,
      tagsPosts,
    },
  }
}

Array.prototype.flatMap() - 配列要素をマップした後、結果を新しい配列内にフラット化する
Array.prototype.some() - 配列をチェックしブール値を返す(true or flase)

5. SEO

ここでは、サイトのメタデータをconfig.jsというファイルにまとめて呼び出す形にしているので、ファイルを作成して下記を記述します。

//config.js

export default {
  title: "SiteName",
  description: "SiteDescription",
  social: {
    twitter: "xxxx",
  },
}

SEO コンポーネントの作成

Next.js では<Head>でメタデータの書き換えができるのでそれで対応しても大丈夫だと思いますが、ここではseo.jsというコンポーネントファイルで管理しています。記事ページなど動的に変更させたい部分の設定をこのファイルに記述しています。

import Head from "next/head"
import config from "../config"

export default function SEO({ pagetitle, pagedesc }) {
  const siteTitle = config.title
  const siteDesc = config.description
  const title = pagetitle ? `${pagetitle} | ${siteTitle}` : siteTitle
  const description = pagedesc || siteDesc

  return (
    <Head>
      <title>{title}</title>
      <meta name="description" content={description} />
      <meta property="og:type" content="website" />
      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
      <meta property="og:site_name" content={siteTitle} />
      <meta property="twitter:card" content="summary" />
      <meta property="twitter:creator" content={config.social.twitter} />
      <meta property="twitter:title" content={title} />
      <meta property="twitter:description" content={description} />
    </Head>
  )
}

記事ページなどで使用するとき

titleは記事データを使用すればいいですが、descriptionも動的に変更させたかったのでマークダウンファイルの記事の書き出し数文字を取得してdescriptionにするためにgetStaticProps()に下記を記述します。

export async function getStaticProps({ params }) {
  ...

  const excerpt = (post.contentHtml).replace(/(<([^>]+)>)/gi, '')

  return {
    props: {
      post,
      prevPost,
      nextPost,
      excerpt: excerpt.slice(0, 160), //抜き出す文字数を設定
    }
  }
}

.replace(/(<([^>]+)>)/gi, '') - HTML タグの削除

メタデータの書き換えをするときは下記のように記述するとできます。

<SEO pagetitle={post.title} pagedesc={excerpt} />