Nuxt × processmd × Netlify でブログサイトを作る

## はじめに
この記事は[JAMstack Advent Calendar 2019](https://qiita.com/advent-calendar/2019/jamstack)の12日目の記事です。
JAMstackについては既に今回のアドベントカレンダーの記事でも他の方が記事を書いてくださっているので省略いたします。

今回の記事ではNuxt.jsのジェネレート機能を使って記事をマークダウンで書くことができるブログサイトについて紹介します。
また、Nuxt初心者なので間違えている部分も多くあるかもしれませんがご容赦くださいm(_ _)m

## 今回利用した主なもの
- Nuxt.js
    - Vue.jsアプリケーションを構築するためのフレームワーク
    - 静的ジェネレート機能も実装されているので今回はこれを使ってジェネレート
- Netlify
    - 静的ホスティングサービス
- Github
    - ここにリポジトリを置いておく
    - GithubのmasterにプッシュされたらNetlifyにデプロイされるように設定をしておく
- processmd 
    - マークダウンをコマンドラインでJSONに変換してくれるnpmモジュール
- TypeScript
    - JSに型をつけることで精神的に優しいコーディング環境を 

## 記事更新フロー
今回は下記のようなフローで記事を更新できるようにしました。

- 1. Markdownで記事を書く
- 2. processmdでMarkdownからJSONを生成
- 3. Nuxt.jsでJSONを元に記事ページをジェネレート
- 4. Githubにコミット＆プッシュ
- 5. プッシュを受けてNetlifyに自動デプロイ

## プロジェクトの作成
### Nuxtプロジェクトの立ち上げ
`create-nuxt-app` でサクッと立ち上げます。
今回はこんな感じで設定を行いました。

```shell
create-nuxt-app v2.12.0
✨  Generating Nuxt.js project in .
? Project name nuxt-markdown-demo
? Project description My fantastic Nuxt.js project
? Author name Shoch0922
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support
? Choose linting tools ESLint, Prettier
? Choose test framework None
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)
```

その後にTypeScript用の設定をします。
Nuxt2.9でのTypeScript設定は[公式サイト](https://typescript.nuxtjs.org/)のドキュメントがよくできているのでこれを参考にします。

### processmdの設定
まずはprocessmdをnpmでインストール

```sh
npm i processmd -D
```

そして以下のようなnpm scriptを設定しておきます。

```json
{
  "scripts" : {
    "md": "processmd ./content/posts/**/*.md --stdout --outputDir ./content/posts/json > summary.json"
  }
}
```

上のコマンドで`./content/posts/**/`に格納されているMarkdownファイルを元に記事ごとにJSONファイルを生成し
記事ごとのJSONの情報をまとめた`summary.json`を生成してくれます。

## 各記事のMarkdownを作成する
記事用のMarkdownを作ります。

記事のメタ情報的な部分はYAML front matterの形式に合わせて記述します。
それ以外は普通にMarkdownの記述で記事をガシガシ書いていきます。

```md
---
title: hoge
created_at: 2019-12-01
image: dummy.jpg
---

## 見出し 2

本文です。本文です。本文です。本文です。本文です。本文です。
本文です。本文です。本文です。本文です。本文です。本文です。
本文です。本文です。本文です。本文です。本文です。本文です。
本文です。本文です。本文です。本文です。本文です。本文です。

...

```

ここで開発用に書いたサンプル記事をprocessmdを使ってJSONファイルに書き出します。

## Nuxtでの記事データ読み込み
Nuxtで書き出したJSONを読み込んで状態管理しておくstoreを作り、getterを2つ定義します。
一つはfileMapを取得するもの、もう一つはsourceFileArray（元となるmdのパスが格納されている配列）を取得するものです。

```ts
import blogs from '../summary.json'
export type Blogs = typeof blogs

export const state = () => ({
  data: blogs
})

export const getters = {
  getSourceFileArray(state: any) {
    return state.data.sourceFileArray
  },
  getFileMap(state: any) {
    return state.data.fileMap
  }
}

```

## 記事一覧の実装
記事一覧コンポーネントを作成します。

```vue
<template>
  <ul v-if="blogs">
    <li v-for="blog in blogs" :key="blog.id">
      <nuxt-link :to="link(blog.date, blog.title)">
        <figure>
          <img :src="`/images/${blog.image}`" :alt="blog.title" />
        </figure>

        <div class="overlay">
          <h2>{{ blog.title }}</h2>
        </div>
      </nuxt-link>
    </li>
  </ul>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import moment from 'moment'

export declare interface IBlogList {
  id: number
  title: string
  image: string
  date: string
}

@Component
export default class BlogList extends Vue {
  private blogs: Array<IBlogList> = []
  private blog: IBlogList

  private created() {
    const data = this.$store.getters['blogData/getFileMap']

    Object.keys(data).forEach((val, i) => {
      const blog: IBlogList = {
        id: i,
        title: data[val].title,
        image: data[val].image,
        date: moment(data[val].created_at).format('YYYY-MM-DD')
      }
      this.blogs.unshift(blog)
    })
  }

  private link(date: string, sulg: string) {
    return `/blog/${date}/${sulg}`
  }
}
</script>
```

かなりシンプルな作りになりました。
storeのgetterを叩いてfileMapを取得し、各記事のデータをforEachで取り出します。
取り出したデータを実際に表示するためにメンバ配列に格納し、この配列をv-forでループし仮想DOMをレンダリングします。

各記事ページへのリンクのパラメータとして`date`と`title`を使います。
このURLを作成するためのメソッドを用意し、各ページへのリンク先を指定します。

これで記事一覧は完成です。

## 記事ページ
最後に記事の個別ページを実装します。

```vue
<template>
  <div class="wrapper">
    <Header />
    <main>
      <div class="container">
        <div
          class="content"
          v-html="sanitizeHTML(bodyHtml, { allowedTags: false })">
        </div>

        <div class="return">
          <nuxt-link to="/">TOPへ戻る</nuxt-link>
        </div>
      </div>
    </main>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import sanitizeHTML from 'sanitize-html'

import Header from '~/components/Header.vue'

@Component({
  components: {
    Header
  }
})
export default class BlogSingle extends Vue {
  private sanitizeHTML: sanitizeHTML = sanitizeHTML

  validate({ params, store }) {
    return store.state.blogData.data.sourceFileArray.includes(
      `./content/posts/${params.date}-${params.sulg}.md`
    )
  }

  asyncData({ params }) {
    return Object.assign(
      {},
      require(`~/content/posts/json/${params.date}-${params.sulg}.json`),
      {
        params
      }
    )
  }
}
</script>
```

`validate`メソッドを使って実際にその記事のMarkdownファイルが存在しているかをチェックします。
バリデートを通過したら`asyncData`メソッドを使ってコンポーネントのローディング前に該当記事のjsonデータを読み込みます。

読み込んだデータを元に`v-html`で表示をします。
自分で書いたMarkdownを表示してるだけなので、セキュリティリスクはほとんどありませんが気持ちsanitizeHTMLを使ってサニタイズします。

あとは良しなにCSSを当てて上げればブログの完成です。

## デプロイ
まずはGithubに任意のリポジトリを作ります。
そしてNetlifyで新しいプロジェクトを作ります。Githubへ認証をすれば許可してるリポジトリが勝手に出てくるので便利です。

TypeScript化したNuxtプロジェクトをNetlifyにデプロイする肝はこの設定です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109953/fda954cb-9624-71b2-c849-88f8719b0fe0.png)

Build Commandの部分をしっかりTypeScript対応させます。
そして公開するディレクトリを選んでデプロイします。

ビルドが無事に完了したらサイト公開完了です！
https://quirky-almeida-833f9a.netlify.com/

## まとめ
Nuxtを使ってMarkdownで書かれた記事からJAMstackなブログを作ることができました！
Netlifyを使うことによってGithubにコミット＆プッシュをするだけでデプロイが完了するので運用もかなり簡単なサイトを構築することができました。