Next.jsでmarkdownを使ったブログでファイルをディレクトリ管理する

# 概要、内容

- Next.jsを使ってmarkdownファイルでブログを作成し、markdownファイルはディレクトリで管理できるようにする
    - `posts`ディレクトリに`.md`ファイルを入れ込んで管理するようなものはいくつか見当たるが、`.md`をどうしてもディレクトリで管理しているいい感じのコードが落ちてなかったので、つまみ食いして改造しました。
    - 今回の解説では`src/_posts/foo.md`と`src/_posts/bar/baz.md`を作成。
- markdownファイルでは`front-matter`を採用し、記事のmeta情報を管理できるようにする
    - 今回の例では`date`と`title`のみ。
- prismでいい感じにスタイルをつける準備をするところまでやっていく


# 目指すディレクトリ構造
今回解説する内容のみ記載

```text
src
├ lib
│ └ api.ts
├ _posts            
│ ├ foo.md
│ └ bar　　　　　　<= これがしたい！
│   └ baz.md
└ pages
  └ posts
     └ [...slug].tsx 

```

- `src/lib/api.ts` : build時に実行されるメソッドを定義(markdownファイルの情報取得するメソッドが入ってる)
- `src/_posts/**/*.md` : 記事となるmarkdownファイル
- `pages/pages/posts/[...slug].tsx` : markdownファイルのファイル名の通りにルーティングされるようにする
- `[...slug].tsx` : `dynamic-routing`というやつ

# 先に必要なモジュール入れる

```terminalal
# markdownをhtmlに変換してくれるやつ(たぶん)
$ yarn add remark remark-html

# markdownファイルのパスを取得するのに使う
$ yarn add grob

# front-matterを解析するやつ(markdownの---で区切られたやつをオブジェクトにして返してくれるやつ)
$ yarn add gray-matter

# markdownのコードにスタイルを適用
$ yarn add prismjs @types/prismjs
$ yarn add -D babel-plugin-prismjs
```

## front-matter解析とslug取得のメソッド定義

`src/pages/posts/[...slug].tsx`内の`getStaticProps()`, `getStaticPaths()`で使用する便利なメソッドを作っておく。

```api.ts
import fs from 'fs'
import glob from 'glob'
import { join } from 'path'
import matter from 'gray-matter'

// markdownファイルのを追加する場所のpathを定義
const postDirPrefix = 'src/_posts/'
const postsDirectory = join(process.cwd(), postDirPrefix)

// `getStaticProps()`で使用する
// 引数で(["bar", "baz"], ["date", "title", "content"])を受け取った時、
// `bar/baz.md`のファイルの中身を解析していい感じに以下のようなオブジェクトにして返すように
//  {
//    date: "2020-10-01"  // front-matterの情報
//    title: "markdown title"  // front-matterの情報
//    content: "<div>コンテンツ<div>"  // markdownのコードの中身
//  }
export const getPostBySlug = (slugArray: string[], fields: string[] = []) => {
  const matchedSlug = slugArray.join('/')
  const realSlug = matchedSlug.replace(/\.md$/, '')
  const fullPath = join(postsDirectory, `${realSlug}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data, content } = matter(fileContents)

  type Items = {
    [key: string]: string | string[]
  }

  const items: Items = {}

  fields.forEach((field) => {
    if (field === 'content') {
      items[field] = content
    }

    if (data[field]) {
      items[field] = data[field]
    }
  })

  return items
}

// 'src/_posts/**/*.md`のすべてのファイルを取得して配列にして返す
// 'src/_posts/foo.md`と'src/_posts/bar/baz.md'がある場合[["foo"], ["bar", "baz"]]を返す 
export const getAllPosts = () => {
  const entries = glob.sync(`${postDirPrefix}/**/*.md`)
  return entries
    .map((file) => file.split(postDirPrefix).pop())
    .map((slug) => (slug as string).replace(/\.md$/, '').split('/'))
}
```

# 上記で定義したメソッドを使ってPageコンポーネントを作る

```tsx:[...slug].tsx
import { useRouter } from 'next/router'
import ErrorPage from 'next/error'
import Layout from '../../components/Layout'
import { getPostBySlug, getAllPosts } from '../../lib/api'
import Head from 'next/head'
import remark from 'remark'
import html from 'remark-html'

type Props = {
  post: {
    date: string;
    title: string;
    content: string;
  }
}

// getStaticProps()で返された内容が引数で受け取られる
const Post = ({ post }: Props) => {
  const router = useRouter()

  return (
    <Layout>
      {router.isFallback ? (
        <h1>Loading…</h1>
      ) : (
        <>
          <article>
            <Head>
              <title>{post.title} | Next.js Blog Example with</title>
            </Head>
            <div dangerouslySetInnerHTML={{ __html: post.content }} />
          </article>
        </>
      )}
    </Layout>
  )
}

export default Post

type Params = {
  params: {
    slug: string[]
  }
}

const markdownToHtml = async (markdown: string) => {
  const result = await remark().use(html).process(markdown)
  return result.toString()
}


// getStaticProps: build時にデータを取得(各ページに対して1回実行される)
// getStaticPaths() から返された paths の要素をひとつ受け取り、
// 上に書いたPageコンポーネントに渡すパラメータのオブジェクトを返すようにする
export const getStaticProps = async ({ params }: Params) => {
  // 第二引数に渡した配列の要素をkeyとしたオブジェクトが返される(上記で解説)
  // date, titleにはfront-matterの内容が、contentにはmarkdownのコードの中身がvalueとなる
  const post = getPostBySlug(params.slug, [
    'date',
    'title',
    'content',
  ])
  const content = await markdownToHtml((post.content as string) || '')

  return {
    props: {
      post: {
        ...post,  // front-matterのmeta情報(date, title)
        content  // markdownのcontents
      }
    }
  }
}

// getStaticPaths: build時にデータを取得(全体を通して1度だけ実行される)
// pathsとfallbackの2つのキーをもつオブジェクトを返す必要がある
//  pages/posts/[slug].js の場合
//   ex:  return  {
//          paths: [{ params: { slug: "foo" }, { params: { slug: "bar" }],
//          fallback: false
//        }
//
//  pages/posts/[...slug].js の場合 (slugを配列にして返す)
//   ex:  return  {
//          paths: [{ params: { slug: ["foo"] }, { params: { slug: ["bar", "baz"] }],
//          fallback: false
//        }
export const getStaticPaths = async () => {
  let posts = getAllPosts()  // 上記で解説
  return {
    paths: posts.map((post) => {
      return {
        params: {
          slug: post
        }
      }
    }),
    fallback: false
  }
}
```

## prismjsの導入



```javascript:.babelrc.js
module.exports = {
  presets: ['next/babel'],
  plugins: [
    [
      'prismjs',
      {
        languages: ['javascript', 'css', 'html'],
        plugins: ['line-numbers', 'show-language'],
        theme: 'okaidia',
        css: true
      }
    ]
  ]
}
```