概要
microCMS
、Next.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
全体の流れ
- microCMSの準備(アカウント作成・記事コンテンツのAPI作成)
- Next.jsのプロジェクト作成
- APIリクエストのための環境変数の設定
- 公式のSDK「microcms-js-sdk」の導入
- 記事一覧画面の作成
- 記事詳細画面の作成
- コードブロックのシンタックスハイライトの実装
- 【おまけ】タグでの絞り込み機能の実装
- 【おまけ】ページネーション機能の実装
というような流れで説明していきます。
1. microCMSの準備
まずはmicroCMSのアカウントを作成し、APIを作成します。
手順は公式のチュートリアルに詳しく書かれているので、ここでの説明は割愛します。
microCMS + Next.jsでJamstackブログを作ってみよう 2. microCMSの用意する
私は blog
と tag
の2つのAPIエンドポイントを作成しました。
blog
- title:記事タイトル
- body:記事本文
- tags:タグ(別のエンドポイント
/tag
に登録したタグを複数参照できるように設定) - image:表示する画像名(プロジェクトディレクトリに保存した画像の中から、ブラウザに表示する画像を指定するために使用)
tag
ブログ記事に紐づけるタグを登録しておくリスト形式の API です。
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のエラーなども同様に表示してくれます。
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
},
この設定内容は、以下の記事を参照させていただきました。
ESLint
と Prettier
のコード整形がバッティングしないようにするため、以下を追記します。
// .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
}
}
これでESLint
とPrettier
の設定は完了です。
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
serviceDomain
と apiKey
の値は env
ファイルを参照します。
// libs/client.ts
import { createClient } from "microcms-js-sdk";
export const client = createClient({
serviceDomain: process.env.SERVICE_DOMAIN || "",
apiKey: process.env.API_KEY || "",
});
※serviceDomain
と apiKey
は string
型、.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
tags
ではこのデータを画面に表示します。
※ディレクトリ構成は以下のようにします。
-
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
の設定
getStaticPaths
で return { 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の公式の記事でも紹介されている、cheerioとhighlight.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(),
},
};
};
これでサーバーサード側でシンタックスハイライト済の記事データを取得することができます。
※実装においては以下の記事も参考にさせていただきました。
以上でmicroCMS
とNext.js
を組み合わせたブログ構築の基本の手順は完了です。
ここからは、このブログサイトに実装した機能を少しご紹介したいと思います。
8. 【おまけ】タグでの絞り込み機能の実装
先述の通り、今回は blog
と tag
の2つのAPIを作成し、ブログ記事は複数の tag
を持てるようにしました。
そして
- 記事一覧画面で各記事が持つタグを表示
- APIから取得した
tag
の一覧をサイドバーに表示(タグを選んで記事を絞り込む)
を出来るようにしました。
メリット
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に触れたことが無い方も何となく読み取っていただけるかなと思います。)
記事の一覧表示の中で、この記事が持つ tag
も map
で回して表示します。
{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
というライブラリを使って、ページネーション機能を実装しました。
ライブラリについての詳しい説明では割愛し、ここでは実装したコードだけ載せさせていただきます。
使い方は以下の記事を参考にさせていただきました。
実装手順
まずはライブラリをインストールします。
$ yarn add react-paginate
記事一覧画面を編集していきます。
ページネーションの制御に使う state
と、ページネーションをクリックした時に実行するメソッドを定義します。
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",
});
};
記事の一覧表示の箇所を修正します。
// 表示する記事をsliceで抽出して一覧表示
{showBlogs.slice(offset, offset + perPage).map((blog) => (
ページネーションコンポーネントを記事一覧の下に設置します。
// ページネーションコンポーネント
<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.js
とTypeScript
はいつも使っていますが、自分でESLint
やPrettier
の設定をしたことは無く理解せず使っていたので、自分で調べて設定をすると勉強になりました。
このブログは最低限の機能を作ってデプロイしたのでまだまだ未完成ですが、これから少しずつ機能拡大・スタイル修正をしていきたいなと思います。
参考記事