前提
やること
このブログのコア部分のみつくります。
具体的には記事の取得、全文検索、目次の自動作成、シンタックスハイライトの導入など。
やらないこと
microCMSやNext.jsの細かい解説
ESLintやPrettierの導入など
PWAやダークモード対応、ローディング時のプログレスバーの導入(これらは別記事で紹介しています。)
主な使用技術
- TypeScript
- Next.js
- Tailwind CSS
- microCMS
その他のライブラリ等
- highlight.js
- cheerio
環境構築
microCMSは、アカウント作成、ログイン、サービス作成まで済ませておいてください。
以降はフロント側の構築に入ります。
各自、Next.js、TypeScript、Tailwind CSSの環境を準備してください。
特にコードは変更しなくて大丈夫です。
microcms-js-sdkをインストール
npm i microcms-js-sdk
/.env.localを作成し、サービスドメインとAPIキーを設定します。(サービスドメインは環境変数にしなくても良かったかも)
SERVICE_DOMAIN=xxxx
API_KEY=zzzz
/src/libs/client.tsを作成し、クライアントのインスタンスを作成します。
import { createClient } from "microcms-js-sdk";
export const client = createClient({
serviceDomain: process.env.SERVICE_DOMAIN,
apiKey: process.env.API_KEY,
})
これでデータ取得の準備は完了です。
後のシンタックスハイライト導入のために、highlight.jsというライブラリをインストールします。
npm i highlight.js
後の目次自動作成のためにcheerioというライブラリをインストールします。
npm i cheerio
以上で環境構築は終了です。
実装編
microCMSでAPI実装
GUIでAPIの定義をしていきます。
()内はエンドポイントです。
- カテゴリー(category)
- タグ(tags)
- 著者(author)
- ブログ(articles)
- 人気の記事(popular-articles)
ユニーク設定や必須設定はなるべくオンにしたほうが、フロント実装時にエラーハンドリングの処理が減って楽になると思うので適宜調整してください。
以下()内はフィールドの種類です。
カテゴリー & タグ
name(テキストフィールド)
著者
name(テキストフィールド)
ブログ
title(テキストフィールド)
description(テキストエリア)
slug(テキストフィールド)
body(リッチエディタ)
tags(複数コンテンツ参照 - タグ)
人気の記事
articles(複数コンテンツ参照 - ブログ)
以上で最低限のAPIスキーマの実装は完了です。
作成、更新日時や公開、非公開フラグはデフォルトであるので、そこは意識しなくて大丈夫です。
フロントエンド実装
ブログの取得、目次の自動作成、全文検索機能の実装をします。
作成したブログの取得
SDKとgetStaticPropsを使用して取得します。
/src/pages/blog/index.tsxでブログ一覧の取得(ISR)
import type { GetStaticProps, NextPage } from "next";
import { client } from "../libs/client/client";
type Blog = {
title: string;
description: string;
image: {
url: string;
};
body: string;
category: {name: string};
tags: { name: string }[];
createdAt: string;
};
export const getStaticProps: GetStaticProps = async () => {
const blogs: Blogs = await client.get({ endpoint: "blogs" });
return {
props: {
blogs: blogs,
},
revalidate: 60 * 60,
};
};
type Props = {
blogs: Blogs;
};
const Index: NextPage<Props> = (props) => {
return (
<div>
<ul>
{props.blogs.contents.map((blog, index) => {
return (
<li key={index}>
<Link href={`/blog/${blog.slug}`}>
<a>
{blog.title}
</a>
</Link>
</li>
);
})}
</ul>
</div>
);
};
export default Index;
/src/pages/blog/[slug].tsx
import "highlight.js/styles/hybrid.css";
import cheerio from "cheerio";
import hljs from "highlight.js";
import type { GetStaticPaths, GetStaticProps, NextPage } from "next";
import { TableOfContents } from "src/components/blogs/TableOfContents";
import { Tag } from "src/components/blogs/Tag";
import { ClockSvg } from "src/components/icons/svgs/ClockSvg";
import { FolderOpenSvg } from "src/components/icons/svgs/FolderOpenSvg";
import { Layout } from "src/components/layouts/Layout";
import { Headline1 } from "src/components/utils/Headline1";
import { client } from "src/libs/client/client";
import { fixDateFormat } from "src/libs/fixDateFormat";
import type { Blog, Blogs } from "src/types/types";
export const getStaticPaths: GetStaticPaths = async () => {
const blogs: Blogs = await client.get({ endpoint: "blogs" });
const paths = blogs.contents.map((blog) => {
return `/blog/${blog.slug}`;
});
return { paths: paths, fallback: false };
};
export const getStaticProps: GetStaticProps = async (context) => {
const blogs: Blogs = await client.get({ endpoint: "blogs" });
const blogDetail = blogs.contents.filter((blog) => {
return blog.slug === context.params?.slug;
});
// 見出しの作成
const $ = cheerio.load(blogDetail[0].body);
const headings = $("h2").toArray();
const tableOfContents = headings.map((data: any) => {
return {
text: data.children[0].data,
id: data.attribs.id,
level: data.name,
};
});
return {
props: { blogDetail: blogDetail[0], tableOfContents: tableOfContents, parsedHtml: $.html() },
};
};
type Props = {
blogDetail: Blog;
tableOfContents: any;
parsedHtml: any;
};
const BlogDetailPage: NextPage<Props> = (props) => {
return (
<div>
{/* タイトル */}
<h1>{props.blogDetail.title}<h1/>
{/* サムネイル */}
<img
src={props.blogDetail.image.url}
alt=""
/>
{/* カテゴリー */}
<p>
{props.blogDetail.category.name}
</p>
{/* タグ */}
<ul>
{props.blogDetail.tags.map((tag, index) => {
return (
<li key={index}>
{tag.name}
</li>
);
})}
</ul>
{/* 作成日時 */}
<p className="flex items-center text-gray-500">
<ClockSvg className="block w-4 h-4" />
<span className="block">{fixDateFormat(props.blogDetail.createdAt)}</span>
</p>
{/* 目次 */}
<div>
<p className="py-4 font-bold">{props.blogDetail.description}</p>
</div>
<div>
<h3 className="py-4 font-bold text-center">目次</h3>
<TableOfContents tableOfContents={props.tableOfContents} />
</div>
<article
className="pt-4"
// eslint-disable-next-line @typescript-eslint/naming-convention
dangerouslySetInnerHTML={{ __html: props.parsedHtml }}
></article>
</div>
);
};
export default BlogDetailPage;
目次の自動作成
cheerioというライブラリを使用します。
htmlを読み込ませるとクラス名を付与したりしてくれるので、その結果を表示させています。
検索機能の追加
microCMSが全文検索APIも提供しているので、そちらを利用します。
やることは簡単で、クエリパラメーターにキーワードを付与するだけで実装できます。
シンタックスハイライトの導入
highlight.jsを使用します。
こちらもcheerioを応用して、クラス名を付与したりして装飾しています。
↓サンプルコード
// シンタックスハイライトの導入
$("pre").each((_, element) => {
$(element).addClass("hljs block bg-gray-600 rounded-sm overflow-x-auto");
});
$("pre code").each((_, element) => {
const result = hljs.highlightAuto($(element).text());
$(element).html(result.value);
$(element).addClass("p-2 block");
});
まとめ
かなりざっくりで申し訳ないですが、以上がNext.jsとmicroCMSでブログサイトを構築する手順になります!
SDKもとても便利で、爆速でブログの最低限の機能を実装できました。
TwitterなどからDMいただければ質問には答えますので、お気軽にご質問ください!
実際に使っているコードもGitHubで公開していますので、よかったらどうぞ😗