offich.me を支える技術

作成日: 2024-09-26 /

これ何

Nuxt 3 x Cloudflare Pages x nuxt/content でブログを作りました。

作るまでのいきさつや大変だったことをまとめていきます。

モチベーション

まずはブログ作りまでのモチベーションを書きます。自分を知ってもらうを大きなテーマにしていて、そこから細分化したものを書きます。

1. アウトプットの場が欲しかった

普段 web フロント業務を担当して、デザインやアクセシリビリティ、SEO、パフォーマンス、ビルドツールなど、もう一度最初から勉強し直したい気持ちが芽生えてきました。デザイナーさんや PM さんと一緒に仕様を考えたり、フロントエンドチームと一緒に技術的課題の解決を図るにも、インプットの量が絶対的に足りないとここ数年感じました。手元で動かしたりある程度理解しても、せっかくならそれを本番運用するまでのレベルに昇華させないと実務で役に立たないと思っています。

今まで個人開発に力を入れてこなかったので、次へのレベルアップのためにも、何か 0 から作る経験を積みたかったのもあります。 ブログを作るだけなら、多く存在している情報共有サービスやブログ生成サービスを利用すればいいのですが、自分で 0 から作りたいと思りました。

2. アウトプットを 1 つの場所に集めかった

次にアウトプットの場所を 1 つに統合し、自分を知ってもらう相手のハードルをなるべく下げる狙いがありました。

仕事探しでプロフィールになるものを提出するときに、今度はこのブログの URL を渡す予定です。ブログがないと、今までアウトプットで使用したサービスを統一できず、使用したサービスの URL をすべて渡さないといけません。さらにそれぞれのサービスに用途別でアカウントを別々にわけるともう管理が大変です。新しいサービスが普及すると、そちらへのマイグレーションに手間がかかります。

職務経歴書の管理も 1 つにまとめておきかったのもあります。転職活動するたびに職務経歴書と履歴書を更新して〜、そこにいろんなサービスの URL を貼って〜となかなか骨が折れる作業でした。職務経歴書にいろいろなブログのリンクを貼る作業があるが、どうせならこのサイト内の回遊にしたい気持ちがありました。これからの追加開発で、マークダウンに書いた職務経歴書と履歴書を Excel ファイルに出力する npm パッケージを作る予定です。

3. 技術以外のトピックでも書きたい

プログはもちろん技術的なトピックを集める場となりますが、プライベートで起こったことをどこかにまとめたい気持ちもありました。2024 年は特にライフステージの変化が多かったので、そこで起こったことについての時系列、大変だったこと、振り返りを行う場所を作りたかったです。あとは過去なぜかそのような意思決定をしたかを記録に残したいのもあります。当時は懸命に下調べして検討したのに数年経った後決断をするまでの過程を正確に思い出せないのももったいないと感じています。

幸いなことにプライベートも充実しており、ギターや YouTube などの趣味、育児、結婚、引っ越しなど、今振り返ると多くのイベントがあったので、それらをまとめる予定です。他にも生活していく上で書きたいことが多くありました。決して、自分が生活する上で他人へ共有するに値するものはないと感じていましたが、意外にもこのような体験をしたと話をすると、参考になったとフィードバックをくれたり心身に話を聞いてくださる人がいます。自分自身も調べもので他人のプログを見て参考になることが多いので、今度は自分の体験が還元されることを願ってまとめていきます。

4. コメントやいいね機能がないところでブログを書きたかった

アウトプットすることがエンジニアとして大事なことだとは頭では分かっていたが、どうも今まで渋っており腰が重いと感じていました。 理由を深掘りしていくと、アウトプットにおける他人からの評価を気にしている部分が大きかったと気づきました。他人のいいねやシェア数に一喜一憂し変にモチベージョンに影響が出るのと、何かヒットするのかを調査するのに時間がかかり、結局アウトプットできないことが多かったです。

何のためにアウトプットしているのかと考えたら、まずは他人より自分のためと思えば、心理的敷居が下がると感じたので、評価がない場所でアウトプットをしたいと思いました。qiita、zendev、はてなブログ、note はいいねだけではなくシェア数まで表示されるので、今回作成するブログサイトも記事を評価する機能は実装しないです。ただ参考程度に PV やサイト内滞在時間はどれぐらいなのかを知りたいので、改めて計測ツールの勉強も兼ねて、 Google Analytics を入れる予定です。

サマリー

長くなりましたが、以下がモチベーションの 3 行サマリーになります。

  • 自分のことを知ってもらえる場所を作りたかった。
    • 分散するのではなく、1 つにまとまっているのがわかりやすい。
    • 仕事以外の経験・体験をシェアしていきたい。
    • アウトプットの内容を自分軸で決めていきたい。

技術構成

モチベーションをまとめた上で早速作ろうと、次は要件をまとめました。

要件

上に記載があるように、作りたかったものとしては「自分を他人に知ってもらう」がテーマでした。

1. マークダウンでブログを書きたい

今までの現場で、ADR やミーティングの議事録、作業メモ、システムの繋ぎ込みなどはすべてマークダウン記法で書いていました。なので、今回のブログもマークダウン記法が可能であることを要件に入れました。

ブログを書く上で下の要素をパースし、表示できるものが欲しかったです。

  • コードブロック
  • 箇条書き
  • 強調
  • 引用
  • 写真
  • リンクの埋め込み

2. 静的サイトで作りたい

結論、SSG (static-site generation) の静的サイトで進めていこうと決めました。

ブログ内容はリポジトリの中で管理し自分が書き換えるまで内容は変わらなく、ページの内容が動的に変更されるものではないので、ビルド時に内容が決まる SSG モードのアプリケーションで十分と考えました。パフォーマンスや SEO 対策にも貢献できるとも考えています。

他のレンダリングモードの SSR (server-side rendering) は Node.js のサーバーを用意する、 SPA (Single Page Application) はクローラーへの心配があるので、今回はお見送りしました。

あとは SSG アプリケーションの開発経験を積みたかったのもあります。Nuxt.js の SSR と SPA アプリケーションの開発は今までの現場で経験があるのですが、SSG のアプリケーションだけなかったので、改めて知見を貯めたかったです。

3. 自分が慣れしたんだ技術

Nuxt.js や Vue.js の今後を watch し続けたいものがあることから、せっかくの個人開発でも技術的にチャレンジをしたい気持ちを抑えつつ、Nuxt.js で作ることに決めました。自分のメイン技術スタックとなる Nuxt.js のこれからのリリース変更を手を動かして学んでいきたいので、総じてアウトプットできる場所を作りたかったです。手段と目的が逆になっているというツッコミもありつつ、触れたことがない技術については、今度の個人開発で試していきたいです。このブログは長年運用していく予定でもあるので、改善のリリースまでのスピードを考えると、慣れしたんだ技術のほうがより叶うと考えました。

他のレンダリング方法で、island architecture や Next.js が提供する Incremental Site Regeneration を検討しました。しかし、今ある知識の範疇を飛び抜けると、キャッチアップだけでリリースを迎えるのが難しいと感じました。個人的にチャレンジしたい気持ちがあるので、次の個人開発でぜひ再度検討したいです。

技術選定

要件が決まれば、次は使用する技術についでです。主に使用している技術は下になります。

  • フレームワーク: Nuxt.js
  • マークダウン関連: @nuxt/content
  • ホスティング: Cloudflare Pages
  • UI ライブラリ: Nuxt UI

マークダウンについては、元々知っていた @nuxt/content を採用しました。マークダウンのパースだけではなく、生成したクエリをコンポーネントに渡すと、クエリに合った条件の記事を表示するなど便利なものがあります。パースだけではなくて、こういった取得部分についても、API が提供されているので採用を決めました。このブログを作る上で @nuxt/content の Tips は多くあったので後日まとめていきます。実際運用していて、他人の記事や YouTube、 X(旧 Twitter) のウィジェットを表示できないなど、まだ改修をいれないといけない部分があります。そのような諸々足りない機能についても後日実装していきます。

Nuxt Content made easy for Vue Developers

Nuxt Content made easy for Vue Developers

https://content.nuxt.com

Nuxt Content made easy for Vue Developers

ホスティングでは、Cloudflare Pages を使用することにしました。naked domain を使用できるし、GitHub Actions で CI でデプロイが可能など、運用する上でやりたいことができるので採用しました。デザインシステムのドキュメントや remix でのアプリケーションを本番運用している実績もあるので、改めて自分のブログで使用するにあたって不安がなかったのも大きな理由です。

UI については @nuxt/ui を採用しました。これは 2023 年の VueFes Japan で紹介があり、ぜひ使ってみたいライブラリでした。自分なりのデザインシステムを作るなんてことも面白いかなと一瞬頭をよぎりましたが、一からコンポーネントを作るのも工数が多くかかるので、使うことにしました。

Nuxt UI: A UI Library for Modern Web Apps

Nuxt UI: A UI Library for Modern Web Apps

https://ui.nuxt.com

Nuxt UI: A UI Library for Modern Web Apps

大変だったこと

開発していて、以下が大変だったことです。上記にあるように、SSG アプリケーションの開発の経験がそこまでなく、少し頭を悩ませました。

ページネーション

特にページネーションの部分で悩みました。SPA や SSR アプリケーションを作るときと同じ考えで、ページ数をクエリパラメーターから取得し、動的にページ内部のページ数とコンテンツ一覧を変更できませんでした。SSG ですとページ内部のページ数はビルド時にのみ変更されるので、リクエスト時に変更されるクエリパラメーターを使用してだとうまく動作しません。仮にクエリパラメーターを変更したページのパスで prerender を行っても、パスパラメーターが同じであるため、エラーになってしまいます。そこで、ページのディレクトリ構成を変更し、ページ数をパスパラメーターにしました。

Before

before.txt
src/pages/posts/
├── [...slug].vue
├── categories
│   └── [category].vue // posts/categories/tech?page=1
└── index.vue // posts?page=1

After

after.txt
src/pages/posts/
├── [...slug].vue
├── [page].vue // posts/1
├── categories
│   └── [category]
│       └── [page].vue // posts/categories/tech/1
└── index.vue

prerender のルート選定

繰り返しになりますが、 SSG アプリケーションであるため、事前に prerender するパスを nitro に伝えなければなりません。 以下が prerender させるときのパスの要件でした。

  • ページネーションを効かせる。記事の総数とそれぞれのカテゴリの記事の総数を求める必要がある。
  • draft 記事は含めない。

@nuxt/content の仕様で記事に付与されているカテゴリとそもそも記事が draft かどうかを frontmatter に定義できます。これを prerender するパスにするかいなかの同期をとる必要がありました。

取得するはどうしたらいいかを考えたところ、 remark-mdc というパッケージから parseFrontMatter という処理を使えたので、それをそのまま真似ました。後日詳しく書く予定ですが、rdc parser がすごく便利と感じました。@nuxt/content@nuxt/mdcrdc parser の順番に依存しています。そこで frontmatter 単体をパースしている処理を見つけ、この方法で frontmatter で定義されているデータを prerender のときに取得できました。prerender するパスを決めるスクリプトにデータを定義する必要がなく、これで二重管理を避けられます。

getContentRoute.ts
import { parseFrontMatter } from 'remark-mdc'
import { readFileSync } from 'fs'

const getFrontMatter = (markdownPath: string): { categories: string[], draft: boolean } => {
  const { data: frontmatter } = parseFrontMatter(readFileSync(markdownPath, 'utf8'))
  const { categories, draft } = frontmatter
  return { categories: categories || [], draft }
}

for (const markdownFile of markdownFiles) {
  const frontmatter = getFrontMatter(`${markdownFile.path}/${markdownFile.name}`)

  if (frontmatter.draft) continue // ドラフト記事なら、 prerender のルートに入れない。

  for (const category of frontmatter.categories) {
    // ...category
  }
}

Nuxt UI のカスタマイズ

vuefes で紹介されたので、工数削減のためと OSS のライブラリが改めてどのようにコンポーネントを作成しているのかのインプットのために、使ってみてました。@nuxt/content@nuxt/ui の親和性も高く、使用したいコーディングブロックのコンポーネントも作成できたので、採用して良かったかと思いきや、他のカスタマイズが難しかったです。

それぞれのコンポーネントで、内部のスタイルが TailwindCSS に大きく依存しているので、設定ファイルを変更すると、@nuxt/ui のコンポーネント群のスタイルも大きく変わります。特に自分好みのスペーシングを定義したところ、コンポーネント内部のパーツの大きさを変更するのに、調整に時間がかかりました。

今振り返ると Vue.js 用の headless/ui ライブラリを使えば、カスタマイズが容易だったかと感じます。

rehype plugin のカスタマイズ

マークダウンを書き、実際に表示される内容の QA をしたところ、外部サイトが別タブで開かない問題がありました。せっかくなら自身のブログサイトに留まってほしいので、@nuxt/content の中身をコードリーディングしたところ、rehype plugin というのを見つけました。

rehype についての記事も後日書きますが、HTML に変更を加えるのが容易になります。様々なプラグインがあり、今回 a タグに変更を追加するのは rehype-external-plugin というものでした。そのデフォルトの挙動を見ると、rel: nofollow をつけることがわかります。幸い @nuxt/contentrehype-external-plugin へのオプションを渡してくれるので、a タグに付与したい属性を nuxt.config.ts に定します。これで外部サイトを別タブで開くことができました。

export default defineNuxtConfig({
  markdown: {
    rehypePlugins: [
      [
        'rehype-external-links', {
          target: '_blank',
          rel: 'nofollow noopener noreferrer',
        },
      ],
    ],
  },
})

これからやること

無事にブログをリリースをしましたが、やりたいことが多くあります。

特に Google 関連のツールとの繋ぎ込みに力を入れたいです。

  • Google Search Console に sitemap を登録する。
  • Google Analytics を導入して記事の PV を確認する。
  • Google Adsense を入れて広告をいれるまでの過程を調査したりパフォーマンスにどれだけ影響がでるのかを知りたい。

まとめ

Nuxt 3 と @nuxt/content を使用して、自分のオリジナルのブログを作ってみました。自分のアウトプットを通して、自分を知ってもらう場所ができたことを非常に嬉しく感じます。もちろん開発するまでに詰まったところや学んだところが多くあったので、後日詳細の記事を書いていく予定です。ここまでご精読ありがとうございました。