NuxtでMarkdown記法のブログを作る

このブログはずっと LAMP 環境に WordPress を載せて運営してきたのですが,Markdown 記法を使ってオフラインで記事が書ける環境に少し憧れがありました(Jetpack for WordPress の機能で可能ではあるが).

最近使うことが多い Nuxt.js で作ろうかと思っても DB を用意したり markdownit 等で Markdown を変換したりと面倒に感じていましたが,もっと便利に扱える @nuxt/content というパッケージがなんだかとても良さげです.

ということで,@nuxt/content を使ってブログを一から作り直しました.

構成の概略

  • @nuxt/content: DB 不要の記事管理
  • Bulma: 軽量な CSS フレームワーク
  • Firebase Hosting: SSG でビルドして公開

@nuxt/content は MD ファイルを ~/content に入れるだけでコンテンツとして利用できます.また,MD ファイルの先頭に YAML 形式で情報を付加することができるので,カテゴリーやタグも自由に実装可能です.

CSS フレームワークには Bulma を採用することにしました.HP には BootstrapVue,最近作ったものではマテリアルデザインの Vuetify を使っているのですが,とっても重いです.Vue に対応していると記述が楽であるものの,CSS+JS なフレームワークなのでパッケージが大きくなっていて読み込みに時間がかかります.CSS フレームワークの中では Element UI の淡い感じも良さそうでしたが,レスポンシブではないので見送りました.

ブログのホスティングには Firebase Hosting を利用します(後述の理由により少し使っただけで閉じた).最初は Vercel を使おうとしたのですが,ビルドはエラーしないもののアクセス中に内部処理でエラーが出るらしく,よくわからずじまいです.HP は Vercel で公開しているので @nuxt/content が合わないのでしょうか…

@nuxt/content の簡単な説明

公式ページが日本語に対応しているのでインストール方法等は省略します(日本語ドキュメントは所々で一部削られているので,全文読みたい場合は英語のほうがよい).

記事の表示

Nuxt.js の便利な点の一つに,ルーティングの自動化があります.~/pages 以下のディレクトリ構造でルーティングが認識されるため,これを利用して記事ごとの URL を動的に張ることができます.

そもそもの Vue.js では動的ルーティングは _ で始まるファイル or ディレクトリにより実装できるため,例えば ~/content/articles にファイル名を article-1.md として記事を保存しているなら

pages/_slug.vue

<template>
  <article>
    <h1>{{ page.title }}</h1>
    <nuxt-content :document="page" />
  </article>
</template>

<script>
export default {
  async asyncData ({ $content, params }) {
    const page = await $content('articles', params.slug).fetch()

    return {
      page
    }
  }
}
</script>

というファイルを用意することで,http://blog.example.com/article-1 で記事にアクセスできるようになります.ファイル名ベースで URL を作り,ベージに遷移した際のパラメータを用いて記事の内容を取得するイメージです.

記事リストの表示

$content() は第2引数があれば記事単体を,なければ記事のリストを取得します.例えば,新規作成日(投稿日)順に記事を 10 個とってきたリストは以下のように表示できます.

pages/index.vue

<template>
  <ul>
    <li v-for="article in articles" :key="article.slug">
      <a :href="`/${article.slug}`">{{ article.title }}</a>
    </li>
  </ul>
</template>

<script>
export default {
  async asyncData ({ $content }) {
    const articles = await $content('articles').sortBy('createdAt', 'desc').limit(10).fetch()

    return {
      articles
    }
  }
}
</script>

リンクを張るときに slug を使用すれば,MD ファイル名ベースでの URL が作られます.

$content() をコンテンツをフェッチする際にはメソッドを駆使することで検索機能やソート機能などを付与することができます.

参考:コンテンツを取得する - Nuxt Content

MDファイルの概要

article.title のように取得されているのは,YAML で定義された内容 +α です.MD ファイルは以下のように書くことができます.

---
title: '@nuxt/contentでmd記法のブログを作る'
description: '@nuxt/contentというパッケージがなんだかとても良さげということで,これを使ってブログを一から作り直しました.'
author: mktia

---

このブログはずっと LAMP 環境に WordPress を載せて運営してきたのですが,
Markdown 記法を使ってオフラインで記事が書ける環境に少し憧れがありました
(Jetpack for WordPress の機能で可能ではあるが).

...

上部のハイフンで囲われた部分が YAML 記法に対応しています.上記のように書けば article の属性として title, description, author が取れるわけです.もちろん YAML 記法に則っていれば文字列以外にも配列等の型が利用できます.そのため,記事に複数のタグを付与するなどの柔軟な実装が可能です.

また,ここで定義していなくても自動で割り振られる属性があります.slug はその一つで,ファイル名が入っています.それ以外にファイルの作成日 createdAt やファイルの更新日 updatedAt などもあります.これらの自動で割り振られる内容は YAML で上書きできます.

記事を呼び出したときに返ってくる内容の詳細はAPIエンドポイントを使用することで確認可能です.

目次の取得

article.toc で目次を取得できます.目次は Markdown の見出しタグに従って生成されていて,h2とh3のみが反映されます.

項目の id と文字列,深さが入ったオブジェクトが配列で返ってくるので,うまく処理すれば目次が作れます.

ハイライトシンタックス

ハイライトシンタックスでよく見かけるのは highlight.js ですが,@nuxt/content では Prism.js が使用されています.

Markdown における記法はほぼ同じですが,対応している言語やその指定語が若干異なります.

注意点

asyncDataによる実装

asyncData は非同期でデータを取得しますが,取得し終えるまではページが表示されない仕組みになっています.この関数は pages 以下でしか呼ぶことができないため,例えばカテゴリー一覧を表示するコンポーネントを $content を使って作成して呼ぶみたいなことはできません(データをコンポーネントに投げるのは可能).

SSGでは使えないパッケージ

@nuxtjs/sitemap や @nuxtjs/feed など @nuxt/content と相性が良さそうなパッケージはいくつかありますが,SSG でビルドする場合は $content が呼べずに失敗するので使えないことがあります.

Lighthouseによる評価

公開まで1~2週間はかかりましたが,ようやく完成して(このブログは)WordPress 卒業ということでこれまでと比べて Lighthouse 的にはスコアが上昇したのか,トップページのスコアで比較してみました.

まず,WordPress の頃のスコアがこちら.

PC

84Performance
98Accessibility
86Best Practices
83SEO
0-49
50-89
90-100

Reported by Lighthouse

Mobile

67Performance
98Accessibility
86Best Practices
85SEO
0-49
50-89
90-100

Reported by Lighthouse

KUSANAGI サーバとかではないので WordPress は少し重かったのですが,まあぼちぼちという感じです.

で,Firebase Hosting にしたスコアがこちら.

PC

95Performance
93Accessibility
93Best Practices
100SEO
0-49
50-89
90-100

Reported by Lighthouse

Mobile

71Performance
92Accessibility
93Best Practices
99SEO
0-49
50-89
90-100

Reported by Lighthouse

概ね上昇しました.

ただ,Firebase Hosting は無料枠が転送量上限 10GB /月ですが,このブログは WordPress で約 1GB /日,Nuxt.js で少し軽くなったもののそれでも約 500~600MB /日なので,課金必須になります(いっても安いけど).

それとは別に GMO のホスティングサーバを借りているのでそれでいいかと思って Firebase Hosting から引き上げて CORESERVER に移しました.

PC

100Performance
91Accessibility
93Best Practices
100SEO
0-49
50-89
90-100

Reported by Lighthouse

Mobile

64Performance
92Accessibility
93Best Practices
99SEO
0-49
50-89
90-100

Reported by Lighthouse

モバイル向けスコア,やはり厳しいですね.

トップページで記事一覧だけでなくカテゴリー一覧やタグ一覧を取得している上,Google Analytics と AdSense が他に比べてめちゃめちゃ重いのでどうしようもありません.

最後に

オンラインで記事を書くと,WP を開くまでの手間とかブラウザのページ遷移とかプレビュー生成とか至るところで時間をとられてめんどくさくなってしまうのですが,オフラインだとラグもないですし VS Code の MD プレビューですぐに確認できるのでとても良いです.

作業ログとかをどこでも確認できるようにオンラインにしようとこのブログを作ったので,書くのがめんどくさくなってしまったら元も子もない…

(余談ですが,上の Lighthouse のスコアを表示するためのコンポーネントを作るのがめちゃ面倒でした.)