☀️

Next.js + Markdown + microCMSでブログを作る

2022/05/29に公開

対象

  • Next.jsでブログを作ってみたい方
  • MarkdownやmicroCMSでブログ記事を管理してみたい方
  • データ取得周りが気になる方

コード

こちらに公開してます!

https://github.com/kusakabe-t/blog

ブログの構成

基本的な部分はNext.js + TypeScript + TailwindCSSで作成しています。
その他の記事の管理方法やインフラ周りは以下の表の通りです。

記事の管理方法 説明
microCMS 更新頻度の高い内容や検索したい記事を投稿するために利用
Zenn 独断と偏見で公共性が高めだと感じた記事を投稿
GitHub Zennに投稿するまでもない個人的なメモなどを投稿
記事の公開方法 説明
Vercel Next.jsと相性が良いため利用 (ただしサーバー応答が遅め)
GCP (Cloud Run) サーバー応答を早くするために、Vercelからの移行先として検討中

microCMS

microCMSは無料枠だと3つまでAPIを作成できるため、「タグ」「スクラップ」「スクラップをまとめたもの」の3つを作成しています。
また、それぞれ記事を更新、追加するとVercelのwebhookを叩くようにしてます。

microCMSを使う理由は最近機能リリースのペースが速く、日本での利用も広がっていると感じたためです。
microCMS利用例

microCMS API例

Zenn

ZennではRSSを取得できるため、これを用いてタイトルやURLなどを取得してます。

GitHub

Contentlayer, mdx, markdown-to-jsxなど選択肢は多いですが、このブログではmarkdown-to-jsxを用いて、MarkdownファイルをJSXにしてます。

実装に時間のかかった部分

いくつか手間取った部分があるので共有します。

microCMSのコンテンツ取得APIの作成

Nuxtで作られているmicroCMSの公式ブログのリポジトリを参考に作成しました。

microCMSで作成できるAPIにはオブジェクト形式とリスト形式がありますが、ブログではリスト形式のみを実装しました。
(ちなみに、オブジェクト形式とリスト形式の違いはJSONを返したいか、JSONの配列を返したいかの違いです)

以下のような感じで作りました。
https://github.com/kusakabe-t/blog/blob/master/utils/getContents.ts#L1-L31

サイドバーのデータ生成

microCMSで取得したデータからサイドバーを作りたいときに、毎回APIを叩くとRate Limitにかかります。
https://document.microcms.io/manual/limitations#h1eb9467502

そのため、全てのページで共通の情報はJSONファイルにし各ページにインポートするようにしました。
JSONファイルはビルド時にts-nodeを動かして作ってます。

bin/fetchSidebarData.ts
// #!/usr/bin/env ts-node
import { createJsonFile } from './common/createJsonFile'
import { getAllListContents } from '../utils/getContents'

const folder = 'fetchData/scraps'
const categoriesDataFile = 'scrapLists.json'

export const main = async () => {
  const endpoint = 'scraps'
  const contents = await getAllListContents({
    endpoint,
    queries: {
      fields: 'id,date,createdAt',
    },
  })

  createJsonFile({
    folder,
    filename: folder + '/' + categoriesDataFile,
    content: JSON.stringify(Array.from(contents.values())),
  })
}

main()
bin/common/createJsonFile.ts
import fs from 'fs'

export const createJsonFile = ({ folder, filename, content }: { folder: string; filename: string; content: string }) => {
  if (!fs.existsSync(folder)) fs.mkdirSync(folder)

  fs.writeFileSync(filename, content)
}
tsconfig.json
{
  "compilerOptions": {
    ...
  },
  ...
  "ts-node": {
    "transpileOnly": true,
    "compilerOptions": {
      "module": "commonjs",
      "isolatedModules": true,
    }
  }
}
package.json
{
  ...
  "scripts": {
    ...
    "build": "yarn fetch:scrapLists && next build",
    "fetch:scrapLists": "ts-node ./bin/fetchSidebarDataForNotes.ts",
    ...
  }
}

markdown to jsx

MarkdownファイルからJSXを生成する方法として、Contentlayer, MDX, markdown-to-jsxなどがあります。

Contentlayer

ContentlayerはVercelのエンジニアの方が個人ブログで使用していたため、一度検討してみました (いつの間にか、β版が正式に発表されてました)。
https://www.contentlayer.dev/

結論からいうと、MarkdownからJSONと型情報が生成され、使い勝手は比較的良さげでした。
ただ試した時期が良くなかったのか、動きはするのものの解決の難しいエラーや警告がターミナルにいくつも出てきたため、採用は諦めました。
安定版が出てきたら、また挑戦してみたいです。

MDX

Gatsbyのテンプレートやプロジェクトでは、これがよく利用されています。
https://mdxjs.com/

こちらは@mdx-js/runtimeを使う方法 (非推奨) と@mdx-js/reactを使う方法の2つがあります。

@mdx-js/runtimeを使う場合は以下のようになります。
components = {...} でHTMLタグを上書きし、MDXコンポーネントにMarkdownから取得したデータ (children) を渡して使います。
https://github.com/kusakabe-t/blog/blob/28e5d92b0892b32f7feec901c21264776d499554/components/MdxComponent/Mdx.tsx#L1-L40

この方法はaタグにnext/link, imgタグにnext/imageを当てたりできて便利なのですが、バンドルサイズが大きくなります (babelとかも入るため)。
結果ページ読み込みに時間がかかるようになるので、使い物になりませんでした。。

Next.jsのブログ記事でも言及のある@mdx-js/reactを使う方法では、pages配下にmdxファイルを配置する必要があります。
microCMSとの共存が難しいので、こちらの採用も諦めました。

markdown-to-jsx

最終的にはmarkdown-to-jsxというライブラリを利用することにしました。
こちらのライブラリもHTMLタグの上書きができます。
https://github.com/probablyup/markdown-to-jsx

HTMLタグの上書きができるので、コードブロックはPrismを用いてコードハイライトを有効化、imgタグはクリックで拡大できるようにカスタマイズできます。とても良い。
https://github.com/kusakabe-t/blog/blob/master/components/MdxComponent/Mdx.tsx#L1-L43

markdownからデータを取得する

まだ改良の余地がありますが、globbyやgray-matterを用いてMarkdownファイルからデータを取得するようにしました。
https://github.com/kusakabe-t/blog/blob/master/libs/getContentsFromMdx.ts#L1-L71

globbyは拡張子を指定して該当するファイルパス一覧を取得するために使ってます。
https://github.com/sindresorhus/globby

gray-matterはMarkdownのyamlヘッダーとコンテンツの中身を分離するために使います。
https://github.com/jonschlinkert/gray-matter

コードハイライト

コードハイライト部分はprism-react-rendererを用いて実装してます。
https://github.com/FormidableLabs/prism-react-renderer

基本的には公式のREADMEを読みながらやれば実装できます。
https://github.com/kusakabe-t/blog/blob/master/components/MdxComponent/CodeBlock.tsx#L1-L70

また、prism-react-rendererは一部サポートされてない言語があるため、追加したい言語 (dockerfile, Rustなど) は別途準備しました。

Next.jsでは以下のように対応してます。
https://github.com/kusakabe-t/blog/blob/master/components/MdxComponent/CodeHighlightSupportLanguages.tsx#L1-L9
https://github.com/kusakabe-t/blog/blob/master/components/MdxComponent/CodeBlock.tsx#L26-L34

今後やりたいこと

SSGのページング

Next.jsのRewritesを活用すれば、SSGでページングも実装できるのですが、React18にすると一部挙動がおかしくなるため、原因調査中です。
(以下のコードを追加してページングを作る想定)

next.config.mjs
module.exports = {
  ...
  async rewrites() {
    return {
      beforeFiles: [
        {
          source: '/notes',
          has: [
            {
              type: 'query',
              key: 'pageNumber',
              value: '(?<pageNumber>.*)'
            }
          ],
          destination: '/notes/pageNumber/:pageNumber'
        }
      ]
    }
  }
}

検索機能

コンテンツをmicroCMSで管理している場合は、APIを叩けば良いのですが、その他の記事はどうするか考え中です。

algoliaのような有料サービスを使うか、googleのサイト内検索 (hogehoge site:xxxx.comを叩く) を使うか、30-seconds-of-codeさんのようにJSONファイルを検索するようにするか。

JSONファイルを検索する方式は安くて高速になりそうなので、ここら辺のリポジトリもみながらやりたいなと思ってます。
https://github.com/30-seconds/30-seconds-web/blob/042f968245a2808ed52fc05578fcc675b7af47f7/src/utils/search.js
https://github.com/itemsapi/itemsjs
https://github.com/slmcassio/query-json

インフラ変更

検索機能をどうするか次第ですが、GCPも触ってみたいので、VercelからGCP (Cloud Run) に移行したいなと考えてます。

以上。Next.jsでブログを作る際の参考になれば幸いです。

GitHubで編集を提案

Discussion