Hugoプロジェクトをpnpmモノレポ化してブログにツールを組み込む

ブログに組み込む小さなツールの開発をHugoのjs.Buildからpnpmのモノレポ構成とViteを使った開発環境に移行してみました。

きっかけ

ブログでjs.BuildなどHugoの機能を利用してちょっとしたツールのページを作成していたのですが、ちょっとしたサンプルなどではいいのですが少し規模が大きくなると、あまり開発体験は良いものではありませんでした。

せっかくならViteで開発したいのですが、ブログとは別プロジェクトにするのも管理が面倒です。 Hugoはnpmパッケージのhugo-extendedを利用しているので、pnpmのモノレポ構成にして、両方をうまく管理できないかと試してみました。

プロジェクト構成の変更

ディレクトリ構成について

ディレクトリ構成は以下にしました。

ディレクトリ構成
project-root/
├── blog/                    # Hugoブログ
│   ├── content/
│   ├── layouts/
│   ├── static/
│   ├── assets/
│   ├── config.toml
│   └── package.json
├── tools/                   # 各種ツール
│   ├── tool-01/             # ツール1
│   │   ├── src/
│   │   └── package.json
│   └── tool-02/             # ツール2
│       ├── src/
│       └── package.json
├── package.json
└── pnpm-workspace.yaml

pnpmワークスペースの設定

まずプロジェクトのルートでpnpm-workspace.yamlを作成し以下を設定しました。

pnpm-workspace.yaml
packages:
  - blog
  - tools/*

ルートのpackage.jsonには以下を追加して、pnpm run build:toolsでサブパッケージのツールをビルドするようにしました。 pnpm buildではツールのビルドを行ってからHugoでブログのビルドを行うようにしています。

package.json
{
  "scripts": {
    "dev": "pnpm --filter blog dev",
    "build": "pnpm run build:tools && pnpm run build:blog",
    "build:blog": "pnpm --filter blog build",
    "build:tools": "pnpm --filter @tools/* build"
  }
}

ツールのサブパッケージ化

Viteプロジェクトの作成

以前に作成したPDFメタデータ編集ツールをサブパッケージに移行します。

まずはViteプロジェクトを作成します。

pnpm create vite tools/pdf-meta-editor

package.jsonnameには@tools/pdf-meta-editorを設定しました。

tools/pdf-meta-editor/package.json
{
  "name": "@tools/pdf-meta-editor",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint ."
  }
}

これでプロジェクトのルートでサブパッケージをフィルタしてbuilddevでの起動ができるようになります。

# ビルド
pnpm --filter @tools/pdf-meta-editor build

# 開発モードでの起動
pnpm --filter @tools/pdf-meta-editor dev

# @tools/* をビルド
pnpm run build:tools

Vite設定の調整

Viteの設定を行うvite.config.tsで出力先を調整しました。

tools/pdf-meta-editor/vite.config.ts
export default defineConfig(({ command }) => ({
  base: `/${name.substring(1)}`,
  build: {
    outDir: `../../blog/static/${name.substring(1)}`,
    emptyOutDir: true,
    manifest: true,
    rollupOptions: {
      input: command === 'build' ? 'src/main.tsx' : undefined,
    },
  },
}))

ポイントはbaseにツールを配置するサブパスを指定することと、outDirでHugoのstaticディレクトリに出力することと、manifest: trueでViteのマニフェストファイルを生成することです。

Hugoでのツール読み込み設定

テンプレートの作成

Hugoのテンプレートでは、Viteのmanifest.jsonを読んでファイルを動的に読み込む仕組みを作りました。

blog/layouts/tools.html
{{- with os.ReadFile (path.Join hugo.WorkingDir "static" (printf "%s.vite/manifest.json" .RelPermalink)) }}
  {{- $manifest := . | unmarshal -}}
  {{- with index $manifest "src/main.tsx" }}
    {{- range .css }}
<link rel="stylesheet" href="{{ . }}">
    {{- end }}
<script type="module" defer src="{{ .file }}"></script>
  {{- end }}
{{- end }}

この設定により、Viteでビルドされたファイルを自動的に読み込むことができます。

記事ページの設定

ツールの記事のマークダウンファイルではフロントマターでlayout: toolsを指定することで、上記のテンプレートが利用されツールのページを作成することができます。

---
title: PDF metadata editor
description: 'Tools to edit PDF metadata on the web browser.'
layout: tools
date: 2022-11-29
---

ビルド成果物をGit管理外に設定

Viteでビルドしたファイルはblog/staticに出力されますが、これらはGitで管理する必要がないため.gitignoreを設定しました。

blog/static/.gitignore
tools/

この設定により、ツールのビルド成果物がGitにコミットされることを防げます。

おわりに

モノレポ化したことでViteなどでツールの開発ができるようになったのが大きな改善でした。

今後ツールを追加するときもツールごとに好きな開発ツールを使って開発ができます。ブログとツールを同じリポジトリで管理できて、それぞれ独立して開発できるのがちょうど良いバランスかなと思います。