RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

はじめての Next.js 入門(Headless CMSとの連携)

f:id:moomoo-ya:20210819200645j:plain

こんにちは。 株式会社ラクスで先行技術検証をしたり、ビジネス部門向けに技術情報を提供する取り組みを行っている「技術推進課」という部署に所属している鈴木(@moomooya)です。

今回は最近プライベートで利用するようになったフロントエンドフレームワークNext.jsについて、使えるようになるまでに参考にした情報などをまとめようと思います。

Next.jsはホスティングサービスを提供するVercel社が開発しているReactベースのフロントエンドフレームワークです。このNext.jsをプライベートプロジェクトのサイト構築に利用したケースを元に情報をまとめていきたいと思います。

この記事がこれからNext.jsを触りたいという方の参考になれば幸いです。

Next.jsを利用しようと思った背景

プライベートプロジェクトのWebサイトをGit + SSG1で構成していましたが、共同でプロジェクトを進めているメンバーには非エンジニアもいるため、更新ハードルが高くサイトの更新が属人化していました。旧来のWordpressを使ったサイト構築であれば解決するとは思うものの、せっかくなのでHeadless CMS + SSG(静的ページ)の構成で再構築しようと考えました。

都合が良いことに該当のサイトは

  • 固定ページ
  • ブログ
  • 入力フォーム

を含む程度のシンプルな構成のサイトだったので新しいことを試すのにもってこいでした。

現状では以下のような構成です。

Hexoはシンプルな用途にはnode.jsがあれば動作するためシンプルに使えて便利だったのですが 「『お知らせ』カテゴリのブログ記事を新着から5件トップページに表示したい」 といったことをしようと思うと簡単にはできずモヤモヤしていました。

なのでまずコンテンツ管理はHeadless CMSに切り出して、カテゴリや新着n件といった絞り込みはWeb API経由に任せることにしました。

レイアウトエンジンというかSPAな部分の実装については、表示部品をコンポーネントとして使い回しできるように実装できることを期待して、SSGとしても利用することができるNext.jsを組み合わせることにしました。

Next.jsを使うにあたって参考にした資料

ウェブサイト

React.jsは公式ドキュメントが日本語訳されていますが、Next.js, Vercelについては公式のドキュメントは英語です。

書籍

Reactの書籍は2015年頃にたくさん出版されていますが、Reactの推奨される記述作法が何度か変わっていることから古い書籍はお勧めしません。

また、Next.jsの書籍はほとんどありません。Next.jsについては公式サイトのチュートリアルや、豊富に用意された公式サンプルコードを読み解きながら試してみるのが良いと思います。

和書

  • Reactハンズオンラーニング 第2版
    • ハンズオンとあるものの後述の通りハンズオンではないです
    • JavaScriptの文法→Reactの概念→Reactでの書き方といった、教科書的な構成なので頭から読んでいくのは結構大変かも

洋書

Next.jsで実現したいと思ったことと実現方法

  • Headless CMSから取得したコンテンツを表示
    • ブログだけではなく、固定ページの内容もHeadless CMSで管理
  • コンテンツを部品化し、複数ページで表示

・Headless CMSから取得したコンテンツを表示

Headless CMSには国産Headless CMSサービスのmicroCMS2を採用しています。

ベースはcreate-next-appコマンドで生成しました。

まずはHeadless CMSからコンテンツを取得するライブラリを作ります。

// lib/posts.ts

import axiosBase from 'axios'

// Headless CMSへの基本接続設定
const axios = axiosBase.create({
  baseURL: 'https://hogehoge.microcms.io/api/v1/',
  headers: {
    'X-API-KEY': '(APIキー)',
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  },
  responseType: 'json'
})

// 全ブログの取得(ブログ一覧画面で使用)
export const getAllPosts = async () => {
  const res = await axios.get('/posts')
  return res.data.contents.map((post: any) => {
    return {
      id: post.id,
      title: post.title
    }
  })
}

// 全ブログスラッグ(コンテンツID)の取得(ブログ個別画面で使用)
// ↑のgetAllPostsと統一できそうだけど、試行錯誤中
export const getAllPostIds = async () => {
  const res = await axios.get('/posts')
  return res.data.contents.map((post: any) => {
    return {
      params: {
        id: post.id
      }
    }
  })
}

// 個別ブログの取得(ブログ個別画面で使用)
export const getPostData = async (id: any) => {
  const res = await axios.get('/posts/' + id)
  return res.data
}

ブログ記事一覧を表示したいときは

// pages/blogs.tsx

import Link from 'next/link'
import { getAllPosts } from '../lib/posts'

const IndexContent: NextPage = ({posts}) => {
  return (
    <div>
        <ul>
          {posts.map(post => (
            <li key={post.id}>
              <Link href={`/${post.id}`}>
                <a>{post.title}</a>
              </Link>
            </li>
          ))}
        </ul>
    </div>
  )
}

export const getStaticProps = async () => {
  const res = await getAllPosts()
  return {
    props: {
      posts: res
    }
  }
}

export default IndexContent

といった感じでタイトルとコンテンツIDの取得と表示ができるので、ブログトップの表示はできそうです。関数getStaticPropsはビルド時にNext.jsが呼び出すのでIndexContentに渡すパラメータを取得するために使っています(参考)。

Next.jsではpagesディレクトリ以下のファイル名でルーティング処理が行われるため、pages/blogs.tsxというファイルで実装すればhttps://hogehoge.com/blogsというパスでアクセスできるようになります。

個別ページの表示についてはpages/[id].tsx[]はそのままファイル名)というファイル名で実装すると、パスパラメータを取ることができます。ブログ記事一覧でスラッグをパスパラメータとして渡すようなリンクを生成したので、以下のような実装で個別ページを表示することができます。

// pages/[id].tsx

import * as React from 'react'
import { NextPage } from 'next'
import Link from 'next/link'
import styles from '../styles/Home.module.css'
import { getAllPostIds, getPostData } from '../lib/posts'

type Props = {
  post: object
}

const PostContent: NextPage<Props> = ({ post }) => {
  return (
    <div>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: `${post.content}` }}></div>
    </div>
  )
}

export const getStaticPaths = async () => {
  const paths = await getAllPostIds()
  return {
    paths,
    fallback: false
  }
}

export const getStaticProps = async ({params}) => {
  const post = await getPostData(params.id)    // ここでパスパラメータを取得
  return {
    props: {
      post
    }
  }
}

export default PostContent

基本的な構造はブログ記事一覧の取得と一緒です。関数getStaticPathsは動的ルーティングされるファイルにおいてビルド時に取りうる全パスを取得するためにNext.jsから呼ばれます(参考)。つまりはhttps://hogehoge.com/[id][id]が取りうる値を確定するための関数です。

固定ページについても同様に実装できます。固定ページの場合は、パスパラメータで動的に制御してもいいですし、コンテンツのスラッグと一致させたくない場合はページごとにファイルを作ってスラッグを固定でリクエストしてもいいと思います。

・コンテンツを部品化し、複数ページで表示 → うまくいかなかったorz

冒頭で触れたように

  • 「お知らせ」カテゴリの記事を取得
  • 記事タイトルを5件表示

といったコンポーネントを作ろうと考えていたので、コンポーネント内でHTTPリクエストを投げて表示するような仕組みにできれば実現できるかと思ったのですが、子コンポーネント内でビルド時にHTTPリクエストを投げる手段が見つからず実現に至っていないです。

やるとしたら親となるPageコンポーネント(pagesディレクトリ以下のコンポーネント)で、データを取得して子コンポーネントに渡すことで別コンポーネントにはできますが、データ取得を親に持たせるのが微妙……。

残念ながらやりたいことを実現する方法は見つかりませんでした3。どなたかやり方わかる方いたら教えてください。

他にNext.jsでできること

少し前だとNext.jsというと「SSR用のフレームワークでしょ?」という印象が強かったですが、ここまでやってきたように現在ではSSG用途の機能が充実しています。

また開発元のVercelホスティングサービスを提供しているのですが、Vercelで利用した場合にはISR: Incremental Static Rendering(インクリメンタル静的生成)というSSGした後に差分データの分だけインクリメンタルに動的生成するという挙動もできるようです。

レンダリング以外にもページルーティングだけではなく、APIルーティングにも対応しているため弊社内のBFF4の検証でフレームワークとしてNext.jsを採用したりといった使い方もしています。Next.jsの進化すごい。

Gatsby.jsとの比較

同じようなReactベースのフレームワークとしてGatsby.jsがあります。Gatsby.jsはプラグインシステムであることと内部データをグラフ構造で扱うというところが特徴です。

Markdownからグラフ構造への変換などもプラグインで行うのですが、以前MarkdownとAsciidocを共存させようとした時に変換後のグラフ構造の形式が統一されていなく、結局手動でフォーマットを合わせてマージして、ソートして……という手間が発生して苦労した記憶があります。おそらくGatsby.jsの作法としては「MarkdownとAsciidocの両方に対応した変換プラグインを作る」というのが正しいと思うけど、それぞれのプラグインが存在するのに再発明はしたくなかった……。

今回Next.jsではそこまで触ってはいませんが、Markdownからの変換は直接HTMLに変換するのが一般的なようで手間としてはあまり変わらない印象です。

バックエンドが別途構築されていたり、今回のようなHeadless CMSから取得してレイアウトして表示するような用途だと、グラフ構造に変換したりというのはオーバースペック感があるのでNext.jsの方が使いやすい印象です。

Gatsby.jsはテンプレートに用意されている物をデザインだけ修正して使うような方法だと、裏側で画像の最適化なども特に工夫なくやってくれる5ので個人でブログ作ったりはGatsby.jsの方が使いやすいと思います。

私としてはNext.jsの方がよく触れる分野で有用そうなので、今後の学習時間はGatsby.jsよりもNext.jsに費やしたいと思います。


  • エンジニア中途採用サイト
    ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
    ご興味ありましたら是非ご確認をお願いします。
    20210916153018
    https://career-recruit.rakus.co.jp/career_engineer/

  • カジュアル面談お申込みフォーム
    どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
    以下フォームよりお申込みください。
    forms.gle

  • イベント情報
    会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! rakus.connpass.com


  1. Static Site Generator: Markdownなどからツールを使って静的ページ(単純なHTML + JS + CSSの構成)を生成する仕組み。Gitへのpushをトリガーに再生成と公開サーバーへのデプロイを行うことでウェブサイト更新を行う。

  2. 頻繁にシステムアップデートについてのメールが届くのですが、内容がエンジニア目線で好印象です。関係ないけど会社がご近所。

  3. useStateuseEffectを使って子コンポーネントから取得することはできるのですが、これだとクライアントサイドレンダリングで動いてしまうためSSGが実現できず……。

  4. Backends for Frontends.

  5. Next.jsでも画像最適化機能が実装されたらしいけどまだ使っていない。

Copyright © RAKUS Co., Ltd. All rights reserved.