😃

Next.jsとNetlify CMSでブログを構築してVercelにデプロイ

2022/10/09に公開

当記事では、Next.jsとNetlify CMSを使ってブログを構築する手順について紹介します。

対象読者

  • 記事をGitHubで管理したい、記事はweb上で編集したい
  • 記事をmarkdownで管理したい
  • ブログ運用のランニングコストを抑えたい
    • 他のheadless cmsのように毎月のコストがかからない運用をしたい方
  • Next.jsで構築したい方

Netlify CMS

Netlify CMSは、オープンソースのCMSです。web上でテキストの編集や公開非公開のステータス変更をすることができます。
記事をweb上で作成したり編集すると、GitHubに自動でコミットされます。GitHub ActionsやブランチルールなどのGitワークフローに合わせた使い方ができます。

Netlify CMSは公式のテンプレートがあります。公式テンプレートにはNext.jsで作成してNetilfyにデプロイする例がありますが、今回はデプロイ先をVercelで作ります。

https://www.netlifycms.org/docs/start-with-a-template/

環境

今回は以下の環境で確認しました

  • MacBookPro 13 (2020)
  • node v16.13.0

リポジトリの作成

今回はTypeScriptを採用します。

yarn create next-app sample-blog --typescript

ここは任意ですが、pagesstylesディレクトリをsrcディレクトリ下に移動させます。

mkdir src && \
mv pages/ src/pages & \
mv styles src/styles

yarn devをコンソールで実行して動作を確認します。

markdownを配置するディレクトリを作成

フロント側のUIを作るためにダミーでmarkdownファイルを配置します。ディレクトリ名はcontents

mkdir contents
touch contents/first.md

first.mdの中身は以下のようにします。

---
title: こんにちは
date: 2021-11-22T19:31:20.591Z
cats:
  - description: 'Maru is a Scottish Fold from Japan, and he loves boxes.'
    name: Maru (まる)
  - description: Lil Bub is an American celebrity cat known for her unique appearance.
    name: Lil Bub
  - description: 'Grumpy cat is an American celebrity cat known for her grumpy appearance.'
    name: Grumpy cat (Tardar Sauce)
---

# This page is built with NextJS, and content is managed in Netlify CMS

## Hello

- Nextjs
- Netlify CMS

記事一覧画面を作成

今回はスタイルを細かく作ることはしないのでUIは簡素になります。

pages/index.tsxを編集します。記事の一覧を表示するためには、上記で作成したcontentsディレクトリにあるmarkdownファイルを読み取る必要があります。ファイル読み取りはNode.jsのFileSystemモジュールを使用します。

markdownには記事のbodyの他に日付やタイトルなどのmeta情報(Front Matter)を記述しています。Front Matterはgray-matterというライブラリを利用すると取得が可能です。

https://www.npmjs.com/package/gray-matter

gray-matterを使うことで記事のbodyとfront-matterを取得できます。

import fs from "fs";
import matter from "gray-matter";

export const getStaticProps = () => {
  const files = fs.readdirSync("./content");
  const posts = files.map((fileName) => {
    const slug = fileName.replace(/\.md$/, "");
    const fileContent = fs.readFileSync(`./content/${fileName}`, "utf-8");
    const { data } = matter(fileContent);
    return {
      frontMatter: data,
      slug,
    };
  });

  return {
    props: {
      posts,
    },
  };
};

コンポーネントはpropsで渡された値を以下のように扱います。

import Link from "next/link";

const Blog = ({ posts }: Props) => {
  return (
    <div>
      <p>一覧</p>
      {posts.map((post) => (
        <div key={post.slug}>
          <Link href={`/blog/${post.slug}`}>
            <a>{post.frontMatter.title}</a>
          </Link>
        </div>
      ))}
    </div>
  );
};

export default Blog;

ブラウザで確認してcontents/first.mdのタイトルが表示されていることを確認します。

記事詳細画面の作成

詳細画面はslugの値が異なるので、ページ毎にコンポーネントを作成していると毎回の更新で作業が必要になります。
Next.jsではダイナミックルーティングの機能があります。どのような機能かは公式のドキュメントを見ればわかりますが、ファイル名を[]で囲むと動的なルーティングができるようになります。

https://nextjs.org/docs/routing/dynamic-routes

[slug].tsxを作成します。

const Detail = () => {
  return <div>詳細画面</div>
}
export default Detail

一覧画面の記事タイトルをクリックすると詳細画面に遷移します。すると"詳細画面"という文字列が表示されます。この状態では、記事毎に表示内容が変更できないのでgetStaticProps()を使用してmarkdownのデータを取得します。

ダイナミックルーティングをしているページでgetStaticProps()を使用するときは、getStaticPaths()が必要になります。getStaticPaths()は、ビルド時にページを作成するときに必要なパス情報を管理しています。ダイナミックルーティングで設定されていないとエラーになります。

getStaticPaths()は、contents/ディレクトリの中にあるmarkdownファイル名を取得して記事のURLパス名にする処理を記述しています。

export async function getStaticPaths() {
  const files = fs.readdirSync("./content");
  const paths = files.map((fileName) => ({
    params: {
      slug: fileName.replace(/\.md$/, ""),
    },
  }));

  return {
    paths,
    fallback: false,
  };
}

getStaticProps()は一覧の画面と同じような実装になります。

export async function getStaticProps({ params }: any) {
  const file = fs.readFileSync(`./content/${params.slug}.md`, "utf-8");
  const { data, content } = matter(file);
  return { props: { frontMatter: data, content } };
}

コンポーネントは以下のように実装します。記事bodyはmarkdown記述なのでReactMarkdownでmarkdownからhtmlタグへと変換をします。

https://github.com/remarkjs/react-markdown

import ReactMarkdown from "react-markdown";

const Detail = ({ frontMatter, content }: Props) => {
  return (
    <div>
      <h1>{frontMatter.title}</h1>
      <ReactMarkdown>{content}</ReactMarkdown>
    </div>
  );
};

export default Detail;

markdownからhtmlへの変換は他のもライブラリがあるので好みのものを使うといいと思います。

ここまで実装できると一覧画面と詳細画面の2つが作れます。ブログ機能としては十分でcontents/ディレクトリにmarkdownファイルを置いてビルドすることで記事が更新されていきます。

しかし、毎回記事を書いてGitHubにコミットしていくのは大変面倒です。そこでここからは、Netlify CMSを使用してCMSから記事をコミットできるようにします。CIでビルドのワークフローがあればブラウザ上で操作するだけで記事の公開が容易になります。

Netlify CMSに必要なファイルの追加

next.jsのリポジトリにあるpublicディレクトリにファイルを追加します。

cd public
mkdir admin
cd admin

touch index.html
touch config.yml

/public/admin/index.htmlが管理UIへのエントリーポイントになります。

作成したhtmlファイルに以下を貼り付けます。

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Content Manager</title>
  <script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
</head>
<body>
  <!-- Include the script that builds the page and powers Netlify CMS -->
  <script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
</body>
</html>

動作確認はしていませんがnpmパッケージも存在しているのでこれを使うことも出来そうです。

https://www.npmjs.com/package/netlify-cms-app

config.ymlを以下のように編集します。

backend:
  name: github
  repo: #{repository_name}
  branch: main
  base_url: #{hogehoge.vercel.app/}
  auth_endpoint: api/auth/
media_folder: public/img
public_folder: img
collections:
  - name: "pages"
    label: "Pages"
    files:
    - label: "Home"
      name: "home"
      file: "content/home.md"
      fields:
        - { label: "Title", name: "title", widget: "string"}
        - { label: "Publish Date", name: "date", widget: "datetime" }
        - { label: "Body", name: "body", widget: "markdown"}
        - label: 'Cats'
          name: "cats"
          widget: list
          fields:
            - { label: "Name", name: "name", widget: "string"}
            - { label: "Description", name: "description", widget: "text"}

ここでは、リポジトリやデプロイしているURL、タグなどの設定がまとまっています。

他にもオプションは豊富なので公式のドキュメントを貼っておきます。

https://www.netlifycms.org/docs/configuration-options/

認証

上記の設定が終われば、Netlify CMSで記事を作成してGitHubにコミットが可能になります。しかし、このままだとCMSのURLを知っている人なら誰でも自由に編集や削除が出来てしまう。デプロイをNetlifyにするとGitHub OAuthでいい感じに認証をしてくれるようですが、Vercelにデプロイしたときは自分で対応する必要があります。

最初に、GitHub OAuth認証を挟んでNetlify CMSを使えるようにするNPMパッケージがあるのでこれを利用します。

https://www.npmjs.com/package/@openlab/vercel-netlify-cms-github

パッケージ追加後は、READMEに書いてあるとおりに進めていきます。Next.jsは、pages/apiディレクトリ下にファイルを作成するとAPIを構築することができます。認証に必要なエンドポイントの実装をしていきます。

https://nextjs.org/docs/api-routes/introduction

以下の2つのファイルを作成します。

src/pages/api/auth.js
export { auth as default } from '@openlab/vercel-netlify-cms-github'
src/pages/api/callback.js
export { callback as default } from '@openlab/vercel-netlify-cms-github'

config.ymlauth_endpointはここで作成したルーティングと対応しています。

GitHub OAuth Appの作成

新規でOAuth Appの作成をします。 https://github.com/settings/developers へアクセスしてNew OAuth Appから作成します。

  • Application name
    • 任意の名前をしています、分かりやすいものにしましょう
  • Homepage URL
    • デプロイ先のURLを指定します
  • Authorization callback URL
    • 上記で作成した/src/pages/api/callback.jsと対応したURLにします
    • https://<vercel-domain>/api/callback

登録が完了するとClient IDClient secretsの2つのキーを取得することができます。この2つのキーをVercelの環境変数として登録します。

Vercelに環境変数を登録する

最後にVercelに環境変数を登録します。Vercelで作成したプロジェクトの設定からEnvironment Variablesへと進みます。以下の2つを登録します。

  • OAUTH_CLIENT_ID
    • Client idを指定
  • OAUTH_CLIENT_SECRET
    • Client secretsを指定

ここまででNetlify CMSの構築は完了です。ここまでの設定が問題なくできているとhttps://your-site.com/admin/index.htmlにアクセスするとログイン画面がでてきます。ログインに成功するとNetlify CMSのダッシュボードが表示されます。

記事の作成や編集ができることを確認します

最後に

Next.jsとNetlifyCMSを使ったブラウザからコンテンツ管理できるブログの構築方法について紹介しました。デプロイ先をNetlifyにすると自分で認証周りを用意する必要はありませんが、Next.jsと相性の良いVercelにデプロイしました。
Vercelはコミットすると自動でpreviewサイトを用意したりベースブランチにマージすれば勝手に更新されたりとワークフローが充実しています。

世の中には、他にもたくさんのCMSがありますが要件によってはOSSで使えるNetlify CMSはおすすめです。

後日サンプルコードを公開する予定です

Discussion