63
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

microCMS × Next.js(TypeScript)で個人ブログを作る

Last updated at Posted at 2022-02-27

概要

microCMSNext.js(TypeScript)を使ってブログサイトを作成した手順と、追加で実装した機能について、またその過程で学んだことの備忘録記事です。

基本的には公式のチュートリアル通りに進めていますが、TypeScriptの型付けや記事内のコードブロックのハイライトなど詰まった箇所もあったので、まとめておきたいと思います。

  • デプロイURL

  • GitHub

※この記事ではスタイリングについては解説しません。
MaterialUIを主に使用してスタイリングを行っているので、詳しいコードはGitHubをご覧いただければと思います。

使用技術

  • Next.js 12.0.10
  • React 17.0.2
  • TypeScript 4.5.5
  • ESLint 8.8.0
  • prettier 2.5.1

全体の流れ

  1. microCMSの準備(アカウント作成・記事コンテンツのAPI作成)
  2. Next.jsのプロジェクト作成
  3. APIリクエストのための環境変数の設定
  4. 公式のSDK「microcms-js-sdk」の導入
  5. 記事一覧画面の作成
  6. 記事詳細画面の作成
  7. コードブロックのシンタックスハイライトの実装
  8. 【おまけ】タグでの絞り込み機能の実装
  9. 【おまけ】ページネーション機能の実装

というような流れで説明していきます。

1. microCMSの準備

まずはmicroCMSのアカウントを作成し、APIを作成します。
手順は公式のチュートリアルに詳しく書かれているので、ここでの説明は割愛します。
microCMS + Next.jsでJamstackブログを作ってみよう 2. microCMSの用意する

私は blogtag の2つのAPIエンドポイントを作成しました。

blog

ブログコンテンツを登録しておくリスト形式の API です。
blog.jpg

  • title:記事タイトル
  • body:記事本文
  • tags:タグ(別のエンドポイント /tag に登録したタグを複数参照できるように設定)
  • image:表示する画像名(プロジェクトディレクトリに保存した画像の中から、ブラウザに表示する画像を指定するために使用)

tag

ブログ記事に紐づけるタグを登録しておくリスト形式の API です。
tag.jpg

2. Next.jsプロジェクトを作成する

下記のコマンドを実行してNext.jsのプロジェクトを作成し、続けて開発サーバーを立ち上げます。

$ yarn create next-app --typescript
// プロジェクト名の入力を求められるので入力する

$ cd .\microcms_blog\

$ yarn dev

次にsrcディレクトリを作成して、pagesディレクトリとstylesディレクトリをsrcディレクトリの配下に移動させます。

$ mkdir src && mv pages src && mv styles src

さらに、モジュールのインポートを絶対パスで指定できるよう、ベースURLを src ディレクトリに設定します。

// tsconfig.json
{
  "compilerOptions": {
    // 追加
    "baseUrl": "src"
  }
}

ベースURLの設定についてはこちらの記事を参考にさせていただきました。

ESLint / Prettier

ESLintとPrettierについては、こちらの記事を参考にさせていただきました。

ESLint

Next.js のバージョン11 からは、デフォルトでESLintが搭載されています。

ESLintに関するインストール済のパッケージは以下の2つです。

// package.json
{
  ...
  "devDependencies": {
    "eslint": "8.8.0",    // 構文解析のエンジン
    "eslint-config-next": "12.0.10",    // ESLintのルール
  }
  ...
}

Next.js の新規プロジェクト作成時に生成された .eslintrc は ESLint の設定ファイルを意味しており、デフォルトで eslint-config-next の設定が適用されています。
eslint-config-nextのソースコード:next.js/index.js at canary · vercel/next.js | GitHub

デフォルトで設定されているルールの内容等については、この記事を読ませていただきました。

// .eslintrc.json

{
  "extends": "next/core-web-vitals",    // デフォルトの設定ファイル
}

もしESLintで独自のルールを記述したい場合は、rulesに記述します。

// .eslintrc.json

{
  "extends": "next/core-web-vitals",    // デフォルトの設定ファイル
  "rules": {
    // ルールを記述
  }
}

現在 $ yarn lint コマンドを実行すると構文解析を行うことができる状態ですが、VSCode上でリアルタイムに構文解析を行ってくれる拡張機能をインストールしておきます。

また以下の拡張機能は、画像のようにエラーの波線にマウスをホバーしなくてもエラー内容を表示してくれて便利なのでオススメです。
※ESLintに限らずTypeScriptのエラーなども同様に表示してくれます。
error_lens.jpg


lint コマンドの対象ディレクトリを、srcディレクトリ配下のファイルとなるように設定します。

// package.json

"scripts": {
    "lint": "next lint --dir src"
},

Prettier

Prettierはデフォルトでは含まれていないため、インストールします。

$ yarn -DE add prettier eslint-config-prettier

Prettierの拡張機能もインストールします。

Prettierのフォーマットの設定を記述します。

// package.json
"prettier": {
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "jsxSingleQuote": true,
  "printWidth": 100
},

この設定内容は、以下の記事を参照させていただきました。

ESLintPrettier のコード整形がバッティングしないようにするため、以下を追記します。

// .eslintrc
{
  // "prettier"を追記
  "extends": ["next", "next/core-web-vitals", "prettier"]
}

VSCodeでのファイル保存時に、自動でPrettierによるコード整形が実行されるように設定します。

// .vscode/settings.json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  }
}

これでESLintPrettierの設定は完了です。

3. 環境変数の設定

microCMSのAPIへアクセスする際は、リクエスト先のサービスドメインを指定し、リクエストにそのサービスドメインのAPIキーを含める事でデータを取得することができます。

※microCMSでのAPIキーの作成方法は公式ページを参照ください。
APIキー(X-MICROCMS-API-KEY)


まずは環境変数を管理するファイルを作成します。

.localをつけるとローカル環境で使うことができ、 .developmentをつけると開発環境で使えます。

$ touch .env.development.local

.env.development.localファイルを作成したら、サービスドメインとmicroCMSのAPIキーを書き込みます。

※サービスドメインは、例えば自分のmicroCMSページのURLが https://abc.microcms.io/ であれば abc の部分になります。

SERVICE_DOMAIN=xxxxxxxxxxx
API_KEY=xxxxxxxxxxxx

env ファイルに書いた値は、以下のようにしてプロジェクト内で参照することができます。

process.env.API_KEY

4.microcms-js-sdkの準備

APIリクエストには公式が提供しているmicrocms-js-sdkを使います。

まずはmicrocms-js-sdkをインストールします。

$ yarn add microcms-js-sdk

libs/client.ts を作成してSDKの初期化を行います。

$ mkdir src/libs
$ touch ./src/libs/client.ts

serviceDomainapiKey の値は env ファイルを参照します。

// libs/client.ts

import { createClient } from "microcms-js-sdk";

export const client = createClient({
  serviceDomain: process.env.SERVICE_DOMAIN || "",
  apiKey: process.env.API_KEY || "",
});

serviceDomainapiKeystring型、.envファイルから参照する環境変数は string | undefined型なので、もし以下のように || "" の部分を書かないとエラーが出ます。

serviceDomain: process.env.SERVICE_DOMAIN
// 型 'string | undefined' を型 'string' に割り当てることはできません。型 'undefined' を型 'string' に割り当てることはできません。

※Javascript のa || bは、aを評価してそれがtrue(相当)なら a、そうでないなら bを返します。

5. microCMSから記事データを取得する(一覧画面)

型の事前準備

microCMSから取得するブログ記事とタグのデータ型を定義しておきます。

型定義ファイルを別途作成します。

$ mkdir src/types
$ touch ./src/types/blog.ts

作成したファイルに型を定義します。

今回私が作成したAPIの場合は以下の通りになります。

// src/types/blog.ts

export type Blog = {
  id: string;
  body: string;
  title: string;
  tags: Tag[];
  image: string;
  createdAt: string;
  updatedAt: string;
  publishedAt: string;
  revisedAt: string;
};

export type Tag = {
  id: string;
  tag: string;
  createdAt: string;
  updatedAt: string;
  publishedAt: string;
  revisedAt: string;
};

APIリクエストを行う

getStaticPropsを使ってmicroCMSのAPIを叩き、データを取得します。

pages/index.tsx に以下のように書きます。

// pages/index.js


import Link from "next/link";
import type { InferGetStaticPropsType, NextPage } from "next";
import { client } from "libs/client";    // srcから見た絶対パスで指定
import type { Blog, Tag } from "types/blog";    // srcから見た絶対パスで指定

// microCMSへAPIリクエスト
export const getStaticProps = async () => {
  const blog = await client.get({ endpoint: "blog" });
  const tag = await client.get({ endpoint: "tag" });

  return {
    props: {
      blogs: blog.contents,
      tags: tag.contents,
    },
  };
};

// Props(blogsとtags)の型
type Props = {
  blogs: Blog[];
  tags: Tag[];
};

const Home: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = ({
  blogs,
  tags,
}: Props) => {
  console.log(blogs);
  console.log(tags);
  // ... 続く

ここで http://localhost:3000 にアクセスすると、コンソールにAPIから取得したデータが表示されるはずです。

私の作成したAPIだと、以下の画像のようなデータが取れています。

blogs

blogs.jpg

tags

tags.jpg

ではこのデータを画面に表示します。

※ディレクトリ構成は以下のようにします。

  • pages/index.tsx → 記事一覧画面
  • pages/blog/[id].tsx → 記事詳細画面

一覧画面に記事タイトルをリスト形式で表示し、記事詳細画面へのリンクをつけます。

// pages/index.tsx

const Home: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = ({
  blogs,
  tags,
}: Props) => {
  return (
    <div>
      <ul>
        {blogs.map((blog) => (
          <li key={blog.id}>
            <Link href={`/blog/${blog.id}`}>
              <a>{blog.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  )
}

これで http://localhost:3000 にアクセスすると記事タイトル一覧が表示され、タイトルをクリックすると http://localhost:3000/blog/[microCMSで設定したコンテンツID] のURLへ遷移するはずです。

次にこの遷移先のページを作ります。

6. 記事詳細画面を作成する

一覧画面でブログ記事の詳細画面を作っていきます。

上記のリンク先に指定した通り、 pages/blog/[id].tsに詳細画面を作ります。

まずはファイルを作成します。

$ mkdir src/pages/blog
$ touch ./src/pages/blog/[id].tsx

作成したファイルを、以下のように編集します。

// pages/blog/[id].tsx

import {
  GetStaticPaths,
  GetStaticProps,
  InferGetStaticPropsType,
  NextPage,
} from "next";
import { client } from "libs/client";
import type { Blog } from "types/blog";

// APIリクエストを行うパスを指定
export const getStaticPaths: GetStaticPaths<Params> = async () => {
  const data = await client.get({ endpoint: "blog" });

  const paths = data.contents.map((content) => `/blog/${content.id}`);
  return { paths, fallback: false };
};

// microCMSへAPIリクエスト
export const getStaticProps: GetStaticProps<Props, Params> = async (
  const id = context.params?.id;
  const data = await client.get({ endpoint: "blog", contentId: id });

  return {
    props: {
      blog: data,
    },
  };
};

// Props(blog)の型
type Props = {
  blog: Blog;
};

const BlogId: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = ({
  blog,
}: Props) => {
  return (
    <main>
      <h1>{blog.title}</h1>
      <p>{blog.publishedAt}</p>
			{blog.tags.map((tag) => (
        <li key={tag.id}>
          #{tag.tag}
        </li>
      ))}
      <div
        dangerouslySetInnerHTML={{
          __html: `${blog.body}`,
        }}
      />
    </main>
  );
}

ポイント解説

  • fallback: false の設定

getStaticPathsreturn { fallback: false }; と書いています。

これにより、 http://localhost:3000/blog/[存在しないコンテンツID] のURLにアクセスすると404ページへ遷移するようになります。

fallback: true にして存在しないURLにアクセスした場合はエラー画面がブラウザに表示されてしまいます。

  • dangerouslySetInnerHTML

以下の通り、記事本文は dangerouslySetInnerHTML を通して表示しています。

<div
  dangerouslySetInnerHTML={{
    __html: `${blog.body}`,
  }}
/>

APIから返される記事本文は文字列形式(HTMLタグも文字列として取得される)なので、これをHTMLとして描画するためにdangerouslySetInnerHTML を使っています。

7. コードブロックのハイライトを行う

microCMSのリッチエディタでソースコードとして記述した部分は、

<pre>
  <code>
    // コード
  </code>
</pre>

という形式で取得されます。

このままdangerouslySetInnerHTMLを通すだけでは、何もハイライトされていないコードが表示されるので、ライブラリを使って装飾していきます。

microCMSの公式の記事でも紹介されている、cheeriohighlight.jsというライブラリを使ってサーバーサイドでハイライトを行いました。

まずはライブラリをインストールします。

$ yarn add highlight.js cheerio
$ yarn add --dev @types/highlightjs @types/cheerio

次にブログ記事を生成する際の getStaticProps 内の記述を修正します。
※ライブラリについての説明は上記のmicroCMSの記事などを参照ください。

// pages/blog/[id].tsx

// importを追記
import cheerio from "cheerio";
import hljs from "highlight.js";
import "highlight.js/styles/hybrid.css";

// 中略

export const getStaticProps: GetStaticProps<Props, Params> = async (
  context
) => {
  const id = context.params?.id;
  const blog = await client.get({ endpoint: "blog", contentId: id });
  // 以下の部分を追記
  const $ = cheerio.load(blog.body);
  $("pre code").each((_, elm) => {
    const result = hljs.highlightAuto($(elm).text());
    $(elm).html(result.value);
    $(elm).addClass("hljs");
  });

  return {
    props: {
      blog,
      highlightedBody: $.html(),
    },
  };
};

これでサーバーサード側でシンタックスハイライト済の記事データを取得することができます。

※実装においては以下の記事も参考にさせていただきました。

以上でmicroCMSNext.jsを組み合わせたブログ構築の基本の手順は完了です。

ここからは、このブログサイトに実装した機能を少しご紹介したいと思います。

8. 【おまけ】タグでの絞り込み機能の実装

先述の通り、今回は blogtag の2つのAPIを作成し、ブログ記事は複数の tag を持てるようにしました。

そして

  • 記事一覧画面で各記事が持つタグを表示
  • APIから取得した tag の一覧をサイドバーに表示(タグを選んで記事を絞り込む)

を出来るようにしました。

tag_serch.jpg

メリット

tag のAPIエンドポイントを別途作成する構成にしたことで、以下のようなメリットがあります。

  • サイドバーに設置した tag 一覧の表示順の並べ替えや修正を、コードをいじらずmicroCMSの管理画面で行える。
  • tag 名を修正したい時など、記事のタグを一つひとつ修正しなくともAPIの tag を修正すれば一括して変更できる。

実装方法(記事一覧の表示について)

まずは、 getStaticProps から受け取った記事をそのまま全て表示するのではなく、画面に表示する記事だけを格納する state を別途作成します。

const [showBlogs, setShowBlogs] = useState(blogs)
// 初期値にgetStaticPropsから受け取ったブログ記事データを入れる

そして画面には showBlogs の中身を map で回して一覧表示します。
※絞り込みによって showBlogs に記事が入っていなかった場合のための条件付きレンダリングも記述しておきます。

{!showBlogs.length && <p>There are no posts...</p>}
{showBlogs.map((blog) => (
    // 記事を表示

 

※注
このブログサイトはMaterialUIを使ってスタイリングを行っており、ここからは実際に実装しあ通り、通常のHTMLタグではなくMaterialUIのコンポーネントのタグで表記させていただきます。
(MaterialUIのタグ名を見ればどんな役割の要素なのか想像がつくと思うので、MaterialUIに触れたことが無い方も何となく読み取っていただけるかなと思います。)
 

記事の一覧表示の中で、この記事が持つ tagmap で回して表示します。

{showBlogs.map((blog) => (

    // 中略

    // 持っているtagを全て表示
    {blog.tags.map((tag) => (
      <Typography key={tag.id}>
        #{tag.tag}
      </Typography>
    ))}

実装方法(タグによる絞り込みについて)

tag のAPIエンドポイントから受け取るデータは、以下のtag型の配列でした。

export type Tag = {
  id: string;
  tag: string;
  createdAt: string;
  updatedAt: string;
  publishedAt: string;
  revisedAt: string;
};

なのでまずはこのデータから、tag名だけを抜き出して配列に格納します。

// getStaticPropsで取得したtagsからtag名のみ抜き出す
const tagList = tags.map((tag) => tag.tag);

作成した tagList を使って、サイドバーにタグ一覧を表示します。

<List>
  <Typography>
    # Tags
  </Typography>

  <ListItemButton onClick={() => selectTag("all")}>
    <ListItemText primary="All"/>   // primaryに指定した文字列が表示される
  </ListItemButton>

  {tagList.map((tag) => (
    <ListItemButton key={tag} onClick={() => selectTag(tag)}>
      <ListItemText primary={tag}/>   // primaryに指定した文字列が表示される
    </ListItemButton>
  ))}
</List>

onClick に指定した selectTag() メソッドを実装します。

// タグ絞り込み
const selectTag = (tag: string) => {
  if (tag === "all") {
    setShowBlogs(blogs);
  } else {
    const selectedBlogs = blogs.filter((blog) => {
      const haveTags = blog.tags.map((tag) => tag.tag);
      return haveTags.includes(tag);
    });
    setShowBlogs(selectedBlogs);
  }

  // 画面最上部へスクロールさせる
  window.scrollTo({
    top: 0,
    behavior: "smooth",
  });
};

検索のロジックについては以前別の記事に書いたので、詳しくはこちらをご覧ください。
Reactでリアルタイムの検索機能を実装する - Qiita

以上で「All」を選べば全記事が表示され、タグを選べばそのタグを持つ記事のみが表示されるようになりました。

9.【おまけ】ページネーション機能の実装

react-paginateというライブラリを使って、ページネーション機能を実装しました。

ページネーション.jpg
ライブラリについての詳しい説明では割愛し、ここでは実装したコードだけ載せさせていただきます。
使い方は以下の記事を参考にさせていただきました。

実装手順

まずはライブラリをインストールします。

$ yarn add react-paginate

記事一覧画面を編集していきます。

ページネーションの制御に使う stateと、ページネーションをクリックした時に実行するメソッドを定義します。

pages/index.tsx
import ReactPaginate from "react-paginate";


// 中略

  const [offset, setOffset] = useState(0);    // 何番目の記事から表示するか
  const perPage = 6;    // 1ページあたりに表示する記事数

  const handlePageChange = (data: { selected: number }) => {
    // クリックしたページ数を{selected: 1}のようなオブジェクト形式で引数に受ける
    setOffset(data.selected * perPage)   // 表示する記事の開始位置を変更

    // ページ最上部へスクロールする
    window.scrollTo({    
      top: 0,
      behavior: "smooth",
    });
  };

記事の一覧表示の箇所を修正します。

pages/index.tsx
// 表示する記事をsliceで抽出して一覧表示
{showBlogs.slice(offset, offset + perPage).map((blog) => (

ページネーションコンポーネントを記事一覧の下に設置します。

pages/index.tsx
// ページネーションコンポーネント
<ReactPaginate
   previousLabel={"<"}  // 前のページボタン
   nextLabel={">"}    // 次のページボタン
   pageCount={Math.ceil(blogs.length / perPage)}   // ページ総数
   onPageChange={handlePageChange}   // クリック時のfunction
   containerClassName={"pagination"}   // ページネーションであるulに付くクラス名
   activeClassName={"active"}   // アクティブなページのliに着くクラス名
/>

スタイルを付けます。

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto;
  padding: 40px 0;
}

.pagination li {
  margin: 0 16px;
}

.pagination li > a {
  position: relative;
  font-size: 16px;
  width: 24px;
  height: 24px;
  outline: none;
  z-index: 100;
  cursor: pointer;
}

.pagination a::before {
  content: "";
  display: block;
  position: absolute;
  top: 50%;
  left: 50%;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  z-index: -100;
}

.pagination li.active > a::before {
  background-color: #929191;
}

.pagination li.active > a {
  color: #f2f2f2;
}

以上でページネーションを実装できました。

予めフロント側で全件保持している記事データに対して表示を変えているだけなので、もちろんリロードもなく瞬時に切り替わります。

最後に

microCMSは公式のチュートリアルやブログが豊富で分かりやすく、機能もどんどん拡張されており(最初は公式SDKも無かったらしい)、長く使っていくにも安心だなと思いました。

また業務でNext.jsTypeScriptはいつも使っていますが、自分でESLintPrettierの設定をしたことは無く理解せず使っていたので、自分で調べて設定をすると勉強になりました。

このブログは最低限の機能を作ってデプロイしたのでまだまだ未完成ですが、これから少しずつ機能拡大・スタイル修正をしていきたいなと思います。

参考記事

63
52
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?