So What!?

カテゴリ

記事

連絡先

何か間違っている情報などがあればTwitterにてメッセージください。

ご注意

このBlogはアクセスデータをGoogle Analyticsおよび、Cloudflare Analyticsに送信しています。また研究開発用にSentryにパフォーマンスデータを送信しています。それ以外のデータは収集していません。

Nuxt.jsでBlogを作る

2020年12月15日

正直今更感はあるけど、ずっと下書きで残しておくのももったいないので公開しちゃう。公開せずに後悔するなら公開して後悔しよう。

このBlogはNuxt.jsの「Full static generation」を使って完全静的サイトとして作っている。前はページロード後にマークダウンファイルを読み込んで、マークダウンをHTMLに変換して…ということをブラウザ上でやっていたけど、ビルドするときにやらないとなぁとずっと思っていた。Nuxt.jsのFull static generationが普通に使えそうだったのでこれを機に適用した。

静的ページの吐き出し方

Nuxt.jsで静的サイトを出力する場合はnuxt generateを使う。実際に生成するHTMLはnuxt.config.jsのgenerateプロパティにroute指定するだけでOK。自分の場合は、記事一覧は外部JSONファイルで別管理しているので、JSON読み込んでルートを作る関数を指定している。以下はイメージ。

{
    generate: {
        dir: 'public/',
        routes: (callback) => {
            callback(null, [
                {route: '/page1/', payload: {title, content, date: new Date(date.split('/')), articles}}, // これが1ページ分のデータ
                {route: '/page2/', payload: {title, content, date: new Date(date.split('/')), articles}}
            ]);
        },
        fallback: '',
    }
}

これでgenerateすると「/public/page1/index.html」と「/public/page2/index.html」が吐き出される。

payloadには、各ページに渡したいデータを指定する。ここでは以下のデータを渡している。

  • title: 記事のタイトル。JSONの中に書いてある。
  • content: 記事の内容をmarkedでHTML化したテキスト。リポジトリに置いてあるマークダウンは${title}.mdにしていて、ビルド時にfs.readFileを実行している。
  • date: 記事を書いた日付。JSONの中に書いてある。
  • articles: ブログの全記事の情報。サイドナビに全てのページのリンクを表示するために渡している。

このデータを実際に受け取るのは、.vueファイル側のasyncDataになる。

asyncData(context) {
    const {articles, title, content, date} = context.payload;
    return {
        articles,
        title,
        content,
        date,
    };
},

context.payloadの中に、generate実行時にごにょごにょしたデータが渡ってくるので、あとはそれをもとにレンダリングする処理を入れておけばいいだけ。ブログの全記事の情報を表す「articles」は、サイドナビのコンポーネントだけに渡すのが理想なんだけど、asyncDatapagesディレクトリ配下の.vueファイルでしか使えないのが個人的にはイマイチなところ。

どの.vueファイルをベースとするかはrouteによって決まる。たとえば「/page1/」は「/page1/index.vue」を見にいく。 ということは、ページごとに同じ.vueファイルを用意する必要があるのか?というとそうではない。

自分の場合、記事のURLは「/${記事のタイトル}/」になるので、動的なルーティングを使う必要がある。とはいっても動的にルートを作る場合は_slugディレクトリを作っておく必要がおけばいいだけ。「/_slug/index.vue」を作っておけば「/page1/」も「/page2/」も同じ.vueファイルをベースにHTMLが生成される。

記事ページの他に、一応カテゴリページも用意しているので、それも一緒にroutesにぶちこむ感じにしている。最終的には実際の「nuxt.config.ts」を見てもらうのが早そう。

同じルーティングデータでsitemap.xmlを生成してくれるsitemap-moduleというパッケージもあるのだけど、sitemap.xml用のルートでは末尾の「/」あり、SSG用のルートでは末尾の「/」なしで渡さないといけないっぽい?ので若干冗長な感じになっているが、そんな頻繁に触る部分ではないので、とりあえず見てみぬふりをしている。

Full static generation

実は、Nuxt.jsのFull static generationにはプロジェクトの.vueファイルをクロールして、勝手にルートを生成してくれる機能がある。

おそらく<nuxt-link>を見ているのだろうと思うけど、このBlogのように動的ルーティングしたり、payloadをこねくり回す場合は多分自分でルーティングの設定をする必要がある、気がする。

静的サイトとして出力されるものの、Nuxt.jsの「prefetchLinks」がそのまま効くのはけっこう強いと思う。

例えば、スマホの時はページ一覧がページ下部に表示されるのだけど、以下のキャプチャの左下、リンクが画面に入ったタイミングで、リンク先の「payload.js」が動的にプリロードされて、リンクがクリックされたあとの遷移が超高速になる。

しかもNuxt.js内部でネットワークの状況とかを考慮しながらプリフェッチ処理してくれるので、とても優秀。

余談

余談①

VuePressでよくない?と何度か自問自答したけど、なんだかんだコンポーネント自分で作ったりした方が柔軟性高くていいと思っている。関係ないけど、Nuxt.jsのVue3対応そろそろかなぁ。

余談②

今は記事の一覧をJSON管理にしているが、マークダウンにFrontmatterでヘッダー書いて出力する感じにしようかなと思っているところ。結局データを別管理にして外に置いといたほうがいろいろ使いまわせるんだけど、勝手にデータが更新される方法の方がスマートだよね。

余談③

このBlogはAmazon S3 + Amazon CloudFrontで公開しているので、デプロイもAWS CodePipelineを使っている。 GitHubのWebフック経由でGitHubからソース引っ張ってきて、AWS CodeBuildでビルドして、Code DeployでS3に流すだけ。

AWSは正直よく分からなくても使えてしまうのだけど、料金も安いし、情報もたくさん落ちているので、今のところは満足している。