Next.js+TailwindCSS製Blog構築 vol.3

前回の 記事 の続きとなります

|対象となる方

 HTML、CSS、JavaScriptをチュートリアルレベルで書いたことのある方であればなんとなくで理解できると思います。
 Reactを多少書いたことのある方であれば理解は進むと思います。

|ローダーのセットアップ

まずは、3つのライブラリをインストールしていきます。
ターミナルを開き、アプリのディレクトリに移動し、以下のコマンドを入力してください。

npm install react-markdown gray-matter raw-loader

続いて、row-loader でマークダウンファイルを読み込むための Next.js の設定ファイルを作成していきます。 package.json がある階層と同じ場所にnext.config.jsファイルを作成してください。 next.config.jsですよ!nextconfig.js ではないですからね… このドット(.)が無いだけで私は2時間も時間を溶かしてしまいました(T.T)
ファイル作成ができたら、以下のように記述してください。

module.exports = {
  target: 'serverless',
  webpack: function (config) {
    config.module.rules.push({
      test: /\.md$/,
      use: 'raw-loader',
    })
    return config
  },
}

次に、読み込み用のマークダウンファイルを作成していきます。
public などと同じトップの階層にpostsディレクトリを作成し、その中にmypost.mdファイルを作成し、以下のように記述してください。 改行用に各行の末尾に半角スペース2個含まれたりしていますのでコピペしてしまってください。

---
title: 'ブログのタイトル'
author: 'ブログの筆者'
---
↓ここからマークダウンのボディ↓  
色んなマークダウンの書き方を試してみて、  
`どのように表示されるか`確認してみましょう!
- リスト1
- リスト2
- リスト3

最後にこのマークダウンファイルを読み取る部分の作成をしていきましょう!
[post].jsを編集しましょう!少し長いですが以下のように書き換えてください。

import matter from 'gray-matter'
import ReactMarkdown from 'react-markdown'

export default function BlogPost({ frontmatter, markdownBody }) {
  if (!frontmatter) return <></>

  return (
    <article>
      <h1>{frontmatter.title}</h1>
      <p>By {frontmatter.author}</p>
      <div>
        <ReactMarkdown source={markdownBody} />
      </div>
    </article>
  )
}

export async function getStaticProps({ ...ctx }) {
  const { post } = ctx.params

  const content = await import(`../../posts/${post}.md`)
  const data = matter(content.default)

  return {
    props: {
      frontmatter: data.data,
      markdownBody: data.content,
    },
  }
}

export async function getStaticPaths() {
  const blogSlugs = ((context) => {

    const keys = context.keys()
    const data = keys.map((key, index) => {
      let slug = key.replace(/^.*[\\\/]/, '').slice(0, -3)

      return slug
    })
    return data
  })(require.context('../../posts', true, /\.md$/))

  const paths = blogSlugs.map((slug) => `/post/${slug}`)

  return {
    paths,
    fallback: false,
  }
}

各種ファイルを作成したあと、開発サーバーを立ち上げている場合は再起動してください。 立ち上げていない場合はターミナルにてnpm run devをしてください。
http://localhost:3000/post/mypostにアクセスしてみると以下のような画面がみれましたでしょうか?

いくつかに分けて解説をしていきます!

getStaticProps()

まずは、BlogPostファンクションの下にあるgetStaticProps()からですが、 以前にもこのファンクションの説明はしてありますが、 もう一度コンパクトにお伝えすると データをビルド時にコンポーネントに引き渡すファンクション です。 今回のものでいうとマークダウンファイルのデータを取得・解析し、[post].js のページに引き渡す役割を担っているという感じでしょうか。
今回返却しているデータはfrontmattermarkdownBodyの2つです。 frontmatter には何が入っているのかというと、mypost.md ファイルの冒頭で記述した

---
title: 'ブログのタイトル'
author: 'ブログの筆者'
---

の部分(FrontMatter)をgray-matterというライブラリを用いて解析し、以下のようなJSONデータとして返却しています。

{title: 'ブログのタイトル', author: 'ブログの筆者'}

また、markdownBody の中には、マークダウンファイルで記述した FromtMatter より下のテキストが contentというキーでJSONデータの中にそのまま格納されています。 これについては後述しますが、変換作業が必要です。

ReactMarkdown

BlogPost ファンクションの中に<ReactMarkdown source={markdownBody} />という記述があるかと思います。 こちらはreact-markdownというライブラリを使って、マークダウンで記述されているテキストをHTMLに変換している部分です。 source={markdownBody}という形で引き渡してあげても構いませんし、 <ReactMarkdown># Hello, *world*!</ReactMarkdown>という形で挟んであげても大丈夫です!
次回以降の記事でスタイルを整えていきますが、マークダウンで書かれたテキストが、 どのようなHTMLに変換されているかについては色々と試して確認してみてくださいね @@/

getStaticPaths()

最後にgetStaticPaths()とは、getStaticProps() と同様に Next.js が用意しているファンクションです。 どのようなことができるのかというのをザックリ言ってしまうと、 「ビルド時に特定のデータに基づいて動的ルートを静的に生成する」という感じかなと思います。@@;
ブログでいうと記事が複数ある場合、その記事の全てのルートは確保しておきたいですよね。 その記事のルートを確保するために、今回でいうと、マークダウンファイルの名称をルートとして作成しよう!としているわけです。
つまるところ、pages ディレクトリに post01.mdファイルとpost02.mdファイルが格納されている場合、 /post/post01post/post02のアクセスに関しては記事の表示をするが、 それ以外は404ページ(return 内の fallback: false が該当箇所です!)を表示するというコントロールをおこなっているという感じです!

いつものごとく、詳しくはNext公式getStaticPaths をご確認ください。

機能面はこれで概ね完成ですね!超絶に簡易ですが、ブログシステムができました〜 ^^v

|表示コンテンツの整理

ブログシステムの構築も終盤となってまいりました!もうしばらくお付き合いください。
それではやっていきましょう!まずは、トップページに表示するコンテンツを考えていきます。 トップページでは、ブログ記事の一覧と、記事へのリンクを設置していきたいと思います!
components/PostList.jsを開き、以下のように記述してください。

import Link from 'next/link'

export default function PostList({ posts }) {
  if (posts === 'undefined') return null

  return (
    <div>
      {!posts && <div>No posts!</div>}
      <ul>
        {posts &&
          posts.map((post) => {
            return (
              <div key={post.slug} className="container mx-auto">
                <Link href={{ pathname: `/post/${post.slug}` }}>
                  <div className="text-2xl mt-20 hover:underline hover:text-blue-800">
                    {post.frontmatter.title}
                  </div>
                </Link>
                <div className="flex items-center">
                  {post.frontmatter.author}
                </div>
                <div className="mt-8 mb-10 text-justify">
                  {post.frontmatter.excerpt}
                </div>
                <Link href={{ pathname: `/post/${post.slug}` }}>
                  <a className="underline hover:text-blue-800">続きを読む →</a>
                </Link>
              </div>
            )
          })
        }
      </ul>
    </div>
  )
}

それから、pages/index.jsを以下のように修正してください。

import matter from 'gray-matter'
import Layout from '../components/Layout'
import PostList from '../components/PostList'

const Index = ({ title, description, posts }) => {

  return (
    <Layout pageTitle={title}>
      <div>ここがLayoutコンポーネントのChildren部分です</div>
      <div>{description}</div>
      <PostList posts={posts} />
    </Layout>
  )
}

export default Index

export async function getStaticProps() {
  const configData = await import(`../siteconfig.json`)

  const posts = ((context) => {
    const keys = context.keys()
    const values = keys.map(context)

    const data = keys.map((key, index) => {
      let slug = key.replace(/^.*[\\\/]/, '').slice(0, -3)
      const value = values[index]
      const document = matter(value.default)

      return {
        frontmatter: document.data,
        markdownBody: document.content,
        slug,
      }
    })

    return data
  })(require.context('../posts', true, /\.md$/))

  return {
    props: {
      posts,
      title: configData.default.title,
      description: configData.default.description,
    },
  }
}

また、記事の概要についてもリスト内で表示させたいのでmypost.mdファイルの FrontMatter 部分にexcerpt項目を追加してください。

---
title: 'ブログのタイトル'
author: 'ブログの筆者'
excerpt: 'ブログ記事の概要をここに記述ブログ記事の概要をここに記述ブログ記事の概要をここに記述ブログ記事の概要をここに記述'
---
↓ここからマークダウンのボディ↓  
色んなマークダウンの書き方を試してみて、  
`どのように表示されるか`確認してみましょう!
- リスト1
- リスト2
- リスト3

この状態で画面を確認してみると以下のようになっているかと思います。 少し不恰好ですが、後でレイアウト修正しますので、もうしばらく我慢してください。

現在、記事の件数(マークダウンファイルの枚数)は1件しかありません。この状態では複数ある場合の画面を確認できないので、 postsディレクトリに別のマークダウンファイルを作成してみましょう!
another_post.mdファイルを作成し、中身を記述してください。

---
title: 'ブログのタイトル②'
author: 'ブログの筆者②'
excerpt: 'ブログ記事の概要をここに記述②ブログ記事の概要をここに記述②ブログ記事の概要をここに記述②ブログ記事の概要をここに記述②'
---
ブログ記事の中身です。ブログ記事の中身です。ブログ記事の中身です。ブログ記事の中身です。ブログ記事の中身です。ブログ記事の中身です。

保存後画面の確認をしてください!無事2つ目の記事がリストアップされましたでしょうか^^?

ちなみに、Footerコンポーネントが意図した部分にないですよね ^^;
Lyaout.jsで{children}の div にh-20が当たっているのが原因なのですが、レイアウトの修正自体は後でおこなっていくので、心配しないでください!

気になってムズムズが止まらない方は、<div className="bg-yellow-300 h-20">{children}</div>h-20を削除しておいてください!
次に、ブログ記事詳細ページも修正をしていきましょう。[post].jsファイルを開き、下記のようにしてください。

import Link from 'next/link'
import matter from 'gray-matter'
import ReactMarkdown from 'react-markdown'
import Layout from '../../components/Layout'

export default function BlogPost({ siteTitle, frontmatter, markdownBody }) {
  if (!frontmatter) return <></>

  return (
    <Layout pageTitle={`${siteTitle} | ${frontmatter.title}`}>
        <Link href="/">
          <a className="underline">← トップページに戻る</a>
        </Link>
        <article>
          <h1>{frontmatter.title}</h1>
          <p>By {frontmatter.author}</p>
          <div>
            <ReactMarkdown source={markdownBody} />
          </div>
        </article>
    </Layout>
  )
}

export async function getStaticProps({ ...ctx }) {
  const { post } = ctx.params

  const config = await import(`../../siteconfig.json`)
  const content = await import(`../../posts/${post}.md`)
  const data = matter(content.default)

  return {
    props: {
      siteTitle: config.title,
      frontmatter: data.data,
      markdownBody: data.content,
    },
  }
}

export async function getStaticPaths() {
  const blogSlugs = ((context) => {

    const keys = context.keys()
    const data = keys.map((key, index) => {
      let slug = key.replace(/^.*[\\\/]/, '').slice(0, -3)

      return slug
    })
    return data
  })(require.context('../../posts', true, /\.md$/))

  const paths = blogSlugs.map((slug) => `/post/${slug}`)

  return {
    paths,
    fallback: false,
  }
}

大きな変更点は、レイアウトコンポーネントの反映と、トップページに戻るためのリンクの設置です。
この時点で、URLに値を入力することなく、画面上で全ページへのルーティングが通ったかと思います。 一度存分にルーティングを体感してみてください〜

デザイン修正

表示コンテンツの整理(文言等は未修正だが…)はできましたので、次に、デザインの修正に取り掛かっていきましょう!
まずは、Header から修正します。Header.jsを開き、以下のように修正してください。

import Link from 'next/link'
export default function Header() {
  return (
    <header className="bg-black text-white sticky top-0">
      <nav className="mb-20 flex items-center h-20">
        <Link href="/">
          <a className="pl-8 md:pl-20 lg:pl-40 xl:pl-64 2xl:pl-80">My Blog</a>
        </Link>
        <Link href="/about">
          <a className="pl-20">About</a>
        </Link>
      </nav>
    </header>
  )
}


続いて、Footer の修正をしましょう!Footer.jsを開き、以下のように修正してください。

import Link from 'next/link'
export default function Footer() {
  return (
    <footer className="text-white text-xs">
      <div className="bg-gray-900 flex-col text-center cursor-pointer">
        <Link href="/">
          <div className="h-20 flex justify-center items-center">
            Top Page
          </div>
        </Link>
        <Link href="/about">
          <div className="h-20 flex justify-center items-center border-t border-gray-500">
            About Page
          </div>
        </Link>
      </div>
      <div className="bg-black h-10 flex justify-center items-center">
        ©Daisuke All Rights Reserved.
      </div>
    </footer>
  )
}

続いてLayout.jsの修正を行います!以下のように変更してください。

import Head from 'next/head'
import Header from '../components/Header'
import Footer from '../components/Footer'

export default function Layout({ children, pageTitle }) {
  return (
    <>
      <Head>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>{pageTitle}</title>
      </Head>
      <section>
        <Header />
        <div className="m-8 md:mx-14 lg:mx-40 xl:mx-64 2xl:mx-80 mb-20">
          {children}
        </div>
      </section>
      <Footer />
    </>
  )
}

非常にシンプルなデザインとなっておりますが、Header と Footer、Layout の修正が終わり、 デザイン面は整ってきたのではないでしょうか ^^/

最後に、記事詳細のデザインを少しだけ整えましょう! [post].jsの BlogPostファンクションを以下のように修正してください。

// [post].js の BlogPost ファンクション内を修正
export default function BlogPost({ siteTitle, frontmatter, markdownBody }) {
  if (!frontmatter) return <></>

  return (
    <Layout pageTitle={`${siteTitle} | ${frontmatter.title}`}>
      <Link href="/">
        <a className="underline">← トップページに戻る</a>
      </Link>
      <article className="mt-10">
        <h1 className="text-2xl mb-4">{frontmatter.title}</h1>
        <p className="mb-6">By {frontmatter.author}</p>
        <div>
          <ReactMarkdown source={markdownBody} />
        </div>
      </article>
    </Layout>
  )
}

お気づきかと思いますが、マークダウンで書かれた部分… デザインが無いですよね^^;
ただ、少し長くなってきたということもありますので今回はここまでとします!

よかったらシェアしてね!

この記事を書いた人

Web Developer / Educator