type:any

ブログをNext.js、Contentful、Netliftyの構成にした知見(いい感じのページネーション・カテゴリーアーカイブあり)

{ category: "Front-end" }

このブログは4月にGatsuby.jsで作っていたのですが、ページネーション作りたい、カテゴリーアーカイブ作りたいと思いつつなかなか改修できておらず、この際だから最近熱いNext.jsにいっそ置き換えて、スケーラブルにしていこうと思いリプレイスが昨日完了いたしました。

GitHubでコードを大公開していますので、見たい方はそちらを見てください。
https://github.com/hiroko-ino/myblog

コードの解説をページネーション・カテゴリーアーカイブ中心にしていきます。その他ページに関してはGitHubを見ていただければだいたい分かると思っております。

お断り:Next.jsを学び始めてから1ヶ月も経っていない&リファクタリングもあまり出来ていないので、コードの完成度はまだまだだと思っております。もし、もっと良くするにはこうしたほうがよいよというのがありましたらTwitterまでよろしくお願いします…!

ページネーションの実装

何はともあれ、コードはこちらです。

// components/Pagination.tsx

import Link from 'next/link'

import styled from './Pagination.module.scss'

function Pagination({posts, currentNum}) {
  const getLists = (posts, currentNum) => {
    const list = []
    for (let i = 0; i < Math.ceil(posts.length / 10); i++) {
      list.push(<li key={i + 1}
        className={[styled.item, currentNum === i + 1 && styled.is_active].join(' ')}>
          {currentNum === i + 1 ?
            i + 1 :
            <Link href={`/blog/page/${i + 1}`}><a className={styled.link}>{i + 1}</a><Link>}</li>
      );
    }
    return list;
  }

  const lists = getLists(posts, currentNum);

  return (
    <div className={styled.pagination}>
      <ul className={styled.list}>
        {lists}
      </ul>
    </div>
  )
}

export default Pagination
// pages/blog/page/[num].tsx

import Link from 'next/link'
import Head from 'next/head'

import dayjs from 'dayjs'

import { client } from '../../../libs/contentful'

import Layout from '../../../components/Layout'
import Post from '../../../components/post'
import Pagination from '../../../components/Pagination'

import styled from './[num].module.scss'

const Paged = ({ posts, num, allPosts }) => {
  return (
    <>
      <Head>
        <title>type:any</title>
        <meta name="description" content="フロントエンドのことを中心に、自分の書きたいことを書くブログ"></meta>
        <link rel="icon" href="/favicon.png"/>
      </Head>
      <Layout>
        {posts.length > 0
          ? posts.map((p) => (
              <Post
                key={p.fields.slug}
                title={p.fields.title}
                category={p.fields.category.fields.name}
                slug={p.fields.slug}
                createdAt={p.sys.createdAt}
            />
            ))
          : null}
        <Pagination posts={allPosts} currentNum={Number(num)} />
      </Layout>
    </>
  )
}

export const getStaticPaths = async () => {
  const posts = await client.getEntries({content_type: "blogPost"})

  const paths = [];

  for (let i = 0; i <= Math.floor(posts.items.length / 10); i++) {
    paths.push({
      params: {
        num: (i + 1).toString(),
      }
    });
  }

  return { paths, fallback: false }
}

export const getStaticProps = async ({ params }) => {
  const posts = await client.getEntries({content_type: 'blogPost', order: '-sys.createdAt', limit: 10, skip: (params.num - 1) * 10 })
  const allPosts = await client.getEntries({content_type: 'blogPost'})

  return {
    props: {
      posts: posts.items,
      num: params.num,
      allPosts: allPosts.items
    },
  }
}

export default Paged;

*1/23追記: すみません、、!ページネーションのpushする個数の判定の計算が誤っておりました! < Math.ceil(posts.length / 10)に変更しました!!(これも誤ってたらすみません…)

ポイントとしては、ページネーションにすべての記事を渡して、ページネーションの数を計算しているところ、[num].tsx内でgetStaticPaths内でもまたすべての記事を取得し必要なページ分だけpathに渡しています。
[num].tsxでは、getStaticProps内でgetEntries({content_type: 'blogPost', order: '-sys.createdAt', limit: 10, skip: (params.num - 1) * 10 })というリクエストを送っており、limitで1ページ内に表示する記事の総数、skipで記事の取得開始位置を設定できます。ページネーションコンポーネントに現在のページを渡して、カレント表示も完了です。

カテゴリーアーカイブ

かなり手こずったページです。力作です。

// pages/category/[slug].tsx

import Link from 'next/link'
import Head from 'next/head'

import dayjs from 'dayjs'

import { client } from '../../libs/contentful'

import Layout from '../../components/Layout'
import Post from '../../components/post'

import styled from './[slug].module.scss'

const Blog = ({ posts, category }) => {
  return (
    <>
      <Head>
        <title>{category} | type:any</title>
        <link rel="icon" href="/favicon.png"/>
      </Head>
      <Layout>
        {posts.length > 0
          ? posts.map((p) => (
              <Post
                key={p.fields.slug}
                title={p.fields.title}
                category={p.fields.category.fields.name}
                slug={p.fields.slug}
                createdAt={p.sys.createdAt}
            />
            ))
          : null}
      </Layout>
    </>
  )
}

export const getStaticPaths = async () => {
  const posts = await client.getEntries({content_type: "category"})

  const paths = posts.items.map((post) => ({
    params: {
      slug: post.fields.slug.toString(),
    },
  }))
  return { paths, fallback: false }
}

export const getStaticProps = async ({ params }) => {
  const category = await client.getEntries({content_type: "category", 'fields.slug': params.slug})
  const post = await client.getEntries({content_type: "blogPost", "fields.category.sys.id": category.items[0].sys.id, order: '-sys.createdAt'})

  return {
    props: {
      posts: post.items,
      category: params.slug
    },
  }
}

export default Blog;

ポイントとしては、カテゴリーに関しては私はcontent_typeのcategoryというのをblogPostタイプから参照しているのですが、参照しているcategoryのfieldsをfields.category.fields.slugのようには検索することが出来ません。なので、StaticPathから渡ってきたslugでcategoryのcontent_typeから特定のカテゴリーを検索、そのカテゴリのsys.idを使用して絞られたpostをリクエストしています。何度もAPIを叩いている感じが、少し受け付けないですが、これで実装は出来ました。

感想とTODO

Next.jsの記事検索しようと思ってもNuxtがヒットするのにイライラしながらやりましたが、なんとかいい感じにすることが出来ました。
Gatsubyより直感的に動的ルーティングを理解できたので、私的にはNext.jsはとてもお気に入りです。

TODOとしては
・サイドナビとメニューでuseEffectでAPIを叩いていて、なんだかSSGにした意味をあんまり感じていない
・全体的にAPIを叩く回数を減らしたい
・カテゴリーアーカイブにもページネーションを追加したい。が、動的ルーティングをディレクトリに適用する方法がわからないTT
・その他ソースで汚いところ修正

といった感じでしょうか。

あと、関係ないですが、GitHubでmasterへのプルリク作った時、デプロイ失敗するよーとNetliftyに言われたのにマージしたけど、デプロイがコケただけで本番のサイトに影響なくて、Netlifty優しい、と思った…

まめにこのブログのコードはコミットしていきたいですし、いい感じの知見がえられたら共有します!
この記事も見やすいようにリファクタリングしていきます