Next.js + MDXでブログを自作してみた【後編:実装】

Next.js + MDXでブログを自作してみた【後編:実装】

はじめに

本記事は、「Next.js + MDXでブログを自作してみた【前編:技術選定】」の続編です。
前編をご覧になっていない方でなぜNext.jsやMDXを使うのかに興味がある場合、以下のボタンで前編に戻って前編をご一読なさることをおすすめします。

Next.jsの導入

Next.jsのインストール

ターミナルで以下のコマンドを入力し、Next.jsをインストールします。

terminal
 
  npm create-next-app
 

色々質問に答えると、ディレクトリが作成されます。
作成されたディレクトリに移ってから、ターミナルでnpm run devと入力すると、開発用サーバーが立ち上がります。
リンクをクリックすると、ブラウザで以下の画面が表示されます。

ディレクトリ構造

初期状態では、以下のようなディレクトリの構造になっています。

pages

Next.jsでは、pagesディレクトリにJavaScriptのファイルを作成するだけで自動でルーティングされます
例として、当サイトは以下のような階層構造になっています。

当サイトの階層構造
 
  	├about.html
  	├apps.html
  	├blog.html
  	├contact.html
  	├index.html
  	├privacy-policy.html
  	└blog/
          ├20220523.html
          ├20220527.html
          └20230123.html
 

それに対して、当サイトのビルド前のpagesディレクトリは以下のような階層構造になっています。

pages

  	├about.tsx
  	├apps.tsx
  	├blog.tsx
  	├contact.tsx
  	├index.tsx
  	├privacy-policy.tsx
  	└blog/
          ├20220523.mdx
          ├20220527.mdx
          └20230123.mdx
 

JavaScriptのファイルをpagesディレクトリに置くだけで、それと対応してルーティングされるので非常に便利です
なお、私は以前はReact Routerを使って自分でルーティングをしていました。

public

このフォルダには画像などの静的なアセットを置きます。
このフォルダのファイルはビルド後にそのまま出力されます。
初期状態ではfaviconやsvgファイルなどが格納されており、これらのアセットをindex.tsxで参照しています。

移行作業

元々Reactで当サイトを作成していたため、既存部分の移行はほとんどコピペで済みました。
ただ、既存のサイトのHTMLの構造が良くなく、そのままコピペしたらHydration Errorが起きてしまい、その解消に少し時間がかかりました。
HTMLの構造が良くない例としては、<p>タグの中に<div>タグを入れてしまうことなどが挙げられます。

HTMLの構造がよくない場合、ブラウザが自動で修正してUIを表示してくれますが、そうするとサーバーでレンダリングされたUIと相違してしまいます。
これによりHydrationが失敗することがHydration Errorです。
エラーメッセージにしたがってHTMLの構造を修正すれば解消されます。

MDXの導入

公式ドキュメント

Next.jsの公式サイトにMDXの導入方法があります。
必要なライブラリと、next.config.jsの設定方法について解説されています。

今回は、最低限必要なパッケージである

  • @next/mdx
  • @mdx-js/loader
  • @mdx-js/react

に加えて、

  • remark-slug
  • rehype-pretty-code
  • remark-gfm

をインストールし、next.config.jsで設定しました。

remark-slug

MarkdownのHeading要素(##など)をHTMLの<h>タグに変換するときにidを付与してくれます。
後述のTable Of Contentsの作成で役に立ちます。

rehype-pretty-code

Markdownのコードブロックにシンタックスハイライトを加えてくれます。

remark-gfm

GFMとはGithub Flavored Markdownの略で、その名の通りGithub風のマークダウン記法が使えるようになります。
表やToDoリストを簡単に記述できるようになるので便利です。

記事のページの作成

記事のMDXファイルの構造

前編で紹介したように、MDXではMarkdownにJSX記法を混ぜて使うことができます。
それに加え、通常のjsxファイルと同じようにimportexportを使うことができます。
当サイトでは、記事一覧などに使うためのタイトルなどのメタ情報をmetaとしてexportして他のファイルでも参照できるようにしています。
例えば、この記事のMDXファイルの構造は以下のようになっています。

20230127.mdx
 
  export const meta = {
    title: "Next.js + MDXでブログを自作してみた【後編:実装】",
    tags: ["javascript", "html"],
    imgSrc: "/blog/20230126/next_mdx.png",
    date: new Date(2023, 1 - 1, 27), //月に関しては0-11で指定することに注意!
    outline: "Next.jsとMDXでブログを自作してみました。本記事は後編にあたります。後編では、Next.jsとMDXを用いて具体的にどのようにしてブログを構築するのかについて解説します。",
  };

  # {meta.title}

  ## はじめに
  本記事は、「Next.js + MDXでブログを自作してみた【前編:技術選定】」の続編です。  
  前編をご覧になっていない方でなぜNext.jsやMDXを使うのかに興味がある場合、以下のボタンで前編に戻って前編をご一読なさることをおすすめします。  
 
  //以下本文
 

共通レイアウトの作成

タイトルやサムネイル画像などのOGPの情報やSNSシェアボタンなどの共通部分を各記事のMDXファイル内にいちいち記述するのは面倒ですし、管理も大変です。
そこで、MDXで記述した記事全体をexport defaultでエクスポートして、共通部分のレイアウトをする<PostLayout>に渡して表示させることにしました。

20230127.mdx
 
  export const meta = {
    title: "Next.js + MDXでブログを自作してみた【後編:実装】",
    tags: ["javascript", "html"],
    imgSrc: "/blog/20230126/next_mdx.png",
    date: new Date(2023, 1 - 1, 27), //月に関しては0-11で指定することに注意!
    outline: "Next.jsとMDXでブログを自作してみました。本記事は後編にあたります。後編では、Next.jsとMDXを用いて具体的にどのようにしてブログを構築するのかについて解説します。",
  };

  //追加
  export default ({ children }) => <PostLayout meta={meta}>{children}</PostLayout>

  # {meta.title}

  ## はじめに
  本記事は、「Next.js + MDXでブログを自作してみた【前編:技術選定】」の続編です。  
  前編をご覧になっていない方でなぜNext.jsやMDXを使うのかに興味がある場合、以下のボタンで前編に戻って前編をご一読なさることをおすすめします。  
 
  //以下本文
 
PostLayout.tsx
 
  const PostLayout = ({children, meta}) => {
    return(
      // 共通部分1
      // Headタグなどを設定することで、OGPに対応する

      //記事本文
      { children }
      
      // 共通部分2
      // SNSシェアボタンなど
    )
  }

  export default PostLayout
 

Table Of Contentsの作成

PCなどの広い画面で記事が表示されるとき、画面右端にTable Of Contentsが表示されるようにしたいです。
そこで便利なのがTocbotというライブラリです。
なお、Tocとはtable of contentsの略です。

Tocbotは、<h1>などのHeadingタグを読み取って自動でTable Of Contentsを作成してくれます。
ターミナルでnpm i tocbotと入力してインストールした後で、以下のようにして使います。

PostLayout.tsx
 
  import tocbot from "tocbot";
 
  const PostLayout = ({children, meta}) => {
    useEffect(() => {
      tocbot.init({
        tocSelector: '.auto-toc', //auto-tocクラスの子要素としてTable Of Contentsを作成する
        contentSelector: '.post-main-content',  //post-main-contentクラスの子要素のheadingを走査する
        headingSelector: 'h2, h3, h4',  //h2, h3, h4を対象としてTable Of Contentsを作成する
        hasInnerContainers: true,
        orderedList:false,
      });
      
      return () => tocbot.destroy()
    }, [])

    return(
      // 共通部分1
      // Headタグなど

      //tocbotが作成したTable Of Contentsの表示先
      <div className="auto-toc"></div>

      //記事本文
      //ここのHeadingタグを走査してほしいのでpost-main-contentクラスの子要素にする
      <div className="post-main-content">
        { children }
      </div>
      
      // 共通部分2
      // SNSシェアボタンなど
    )
  }

  export default PostLayout
 
Headingタグにidが付与されていないと、Table Of Contents内のリンクをクリックしてもその項目に遷移しません。
このため、前述のremark-slugを使うなどしてHeadingタグにidを付与する必要があります。

記事一覧のページの作成

blog.tsxファイルを編集し、記事一覧を作成します。
その際に、各記事のタイトルやサムネイル画像などの情報が必要になります。
流れとしては

  1. blogフォルダに入っている全てのMDXファイルを走査して各記事のmetaの情報を収集する
  2. 収集したmetaの情報を<Blog/>に渡す 
  3. 記事の一覧を表示する

という順番になります。
ただし、1.の工程で収集するMDXファイルはビルドされると静的なHTMLファイルになってしまうため、ビルド前に収集する必要があります
そこで、getStaticPropsという特別な関数を使います。

getStaticProps関数でmetaの情報の配列をreturnすると、<Blog/>のpropsとして渡されます。

blog.tsx

  import path from 'path'
  import fs from 'fs'
  import Card from "Cardのパス"

  const Blog = ({ metas }) => {
    return (
      <>
        {
          {/* 3.記事の一覧を表示する */}
          {/* 各記事のレイアウトをCardというコンポーネントで定義しておき、それにmetaを渡す */}
          metas.map(meta =><Card meta={meta} />)
        }
      </>
    )
  }
 
  export const getStaticProps = async (ctx) => {
    //1.blogフォルダに入っている全てのMDXファイルを走査して各記事の`meta`の情報を収集する
    const postDir = path.join(process.cwd(), 'pages/blog/')
    const pathList = fs.readdirSync(postDir)
    const contentsPromise = pathList.map(async (p) => {
      const myModule = await import("../pages/blog/" + p)
      return {
        title: myModule.meta.title,
        tags: myModule.meta.tags,
        imgSrc: myModule.meta.imgSrc,
        date: myModule.meta.date,
        outline: myModule.meta.outline,
        fileName: path.parse(p).name
      }
    })
    const metas = await Promise.all(contentsPromise)

    // 2.収集したmetaの情報を<Blog/>に渡す
    return { props: {metas}} ;
  }

  export default Blog
 

完成!

package.jsonscriptsbuildの箇所を以下のように書き変え、ビルド後にエクスポートされるようにします。

package.json
 
 {
  ...
  scripts:{
    ...
    build:"next build && next export"
    ...
  }
  ...
 }
 

そしてターミナルでnpm run buildと入力して実行することで、outディレクトリに静的なHTMLファイルがエクスポートされ、完成です!

おわりに【後編】

後編では、ブログを具体的にどう実装するのかという点について解説しました。
Next.jsもMDXも非常に強力なツールだと思うので、ブログ以外にもドキュメントの作成など様々なことに使えそうだと感じました。
当面の間、この体制で当サイトを運営していきたいと思います。
前編および後編を併せてかなりの文量になってしまいましたが、ここまでご覧いただきありがとうございました!

目次

Feedback

あなたの一言が大きなはげみとなります!

有効な値を入力してください。
有効な値を入力してください。
有効な値を入力してください。