個人ブログをNext.jsのSSGからHonoのSSGに移行した
「Honoのv4が2月9日にリリースされます」という記事にてHono v4ではSSGモードがサポートされると発表があった。この機能を試す目的で今までNext.jsのSSGモードで構築していた個人ブログをHonoのSSG機能で書き換えた。
- ブログ
- リポジトリ
元の個人ブログではr7kamuraさんのr7kamura/gialog: Blog template to use GitHub Issues as article editor.というテンプレを使っている。これはGitHub IssuesをCMSとして用いて記事を書き、issueの作成などのイベントをフックにしてGitHub Actionsを起動させて記事ファイルを作成、そのデータを元にNext.jsのSSG機能でexportして生成されたファイル群をGitHub Pagesにデプロイするという仕組み。
この中でもGitHub Actionsを起動させて記事ファイルを作成
の部分はr7kamuraさん作のr7kamura/gialog-sync: Custom action to update JSON files for gialog.というGitHub Actionsがあるのでこれを使うだけで良い。つまりIssue作成(記事執筆) -> データ化
の部分は既存の仕組みを流用できる。
なので今回Next.jsのSSGからHonoのSSGに移行するためには記事データから記事の静的ファイル生成の部分だけをうまいこと移行できれば良い。
ブログの構成
元のブログには大きく分けて3つのページが存在する。
1つはトップページ(/
)。ここには記事一覧がずらっと表示される。
次に記事ページ(/articles/:id
)。ここには:id
(issueのid)に対応した記事の中身が表示される。
そして最後はRSS用のfeedのページ(/feed.xml
)だ。
このシンプルなページをSSG機能で作成し配信できればOK。
対応したこと
nodeからbunへの切り替え
Honoはドキュメントやissueの中でもbunをベースに語られている部分があったりしてbunを使った方が良さそうかもというのと、個人的にもローカル開発に関して言えばbunの方が楽だよな〜と思ったのでbunをインストールした。
Hono v4をインストール
SSGが利用できるのはv4で、2024-02-09以前の現在ではまだnpm package化されていない。しかし一応RC版は出てるのでbun install hono@4.0.0-rc.3
とやることでお試し可能になる。
正式リリースされたら最新版にする予定。
(※2月9日更新)
Hono v4が正式にリリースされたので普通にbun install hono
とやればOK。
Next.js依存の機能を排除
コンポーネント
幸いコンポーネントでは複雑な機能は使っておらず、<Link>や<Head>を標準のaタグやheadタグに書き換えただけで済んだ。
getStaticProps/getStaticPaths
getStaticPropsについてはSSGの根幹のところになる。
Next.jsではここで生成する記事データを作成し、コンポーネント側でそのデータを元に記事を作成していた。Hono側でも同様な機能を実現するためにまずは既存のコンポーネントとgetStaticProps
を分離した。
そしてgetStaticProps
相当のことをHonoのルート設定部分で実行している。具体的にはsrc/index.tsx
にて下記のような実装になる。
// issueデータを取得してHomeコンポーネントに渡している
app.get("/", async (c) => {
const issues = await listIssues();
return c.render(
<Layout>
<Home issues={issues} />
</Layout>
);
});
また、/articles/:id
のルートの場合はssgParamsという機能を使う。
ssgParams
はNext.jsでいうところのgetStaticPathsみたいな機能で、今回の場合は全記事のIDをssgParams
内で列挙しコールバック内で利用できるようにしている。
ここで生成したIDを元に各記事データを取得し、それを元に記事ファイルを生成していく流れ。コードとしては下記。
app.get(
"/articles/:id",
ssgParams(async () => {
const issues = await listIssues();
return issues.map((issue: any) => {
return {
id: issue.number.toString(),
};
});
}),
async (c) => {
const issueNumber = parseInt(c.req.param("id"), 10);
if (Number.isNaN(issueNumber)) {
return c.notFound();
}
const issue = (await getIssue({ issueNumber })) as Issue;
if (!issue) {
return c.notFound();
}
const issueComments = await listIssueComments({ issueNumber });
const issues = await listIssues();
const issuesWithoutThis = issues.filter(
(o) => o.number !== issueNumber
) as Issue[];
const pickupArticles = [] as Issue[];
for (let i = 0; i < 3; i++) {
const randomIndex = Math.floor(Math.random() * issuesWithoutThis.length);
pickupArticles.push(issuesWithoutThis[randomIndex]);
issuesWithoutThis.splice(randomIndex, 1);
}
return c.render(
<Layout>
<Article
issue={issue}
issueComments={issueComments}
pickupArticles={pickupArticles}
/>
</Layout>
);
}
);
静的ファイルの配信
Next.jsではアイコン画像などの静的ファイルの配信は/public
ディレクトリに配置するだけで良かったがHonoの場合は少し設定が必要。
まず最低限必要なのはsrc/index.tsx
にて静的ファイルを捌くためのルートを設定すること。
import { serveStatic } from "@hono/node-server/serve-static";
app.use("*", serveStatic({ root: "./static" }));
ここで重要なのはNode.jsのHonoヘルパーを使っている点。
というのもHonoにはBunやDenoなど色々なランタイム用にserveStatic
のヘルパーが用意されているのだが、後述するviteで開発サーバーを立ち上げる時にNode.jsを使わないとエラーが出てしまう。例えばBunのimport { serveStatic } from 'hono/bun'
でexportしたserveStatic
を使ってviteでサーバーを立ち上げるとReferenceError: Bun is not defined
となる。
また静的ファイルへアクセスするには開発環境ではhttp://localhost:5173/static/icon.png
で本番環境ではhttp://localhost:5173/icon.png
のようにしたかったので.env.development.local
を作成し環境変数でVITE_PUBLIC_STATIC_URL=http://localhost:5173/static
のような感じで設定を切り替えられるようにした。
あとは/static
ディレクトリに必要な静的ファイルを配置すればok。
CSSの設定
Next.jsの時はstyles/global.css
に大体のCSSを書き、src/_app.tsx
でimportして適用していた。Honoの場合には標準でCSSヘルパーがあるのでこれを使った。
具体的には下記のようなファイルを作成し、
import { css } from "hono/css";
export const globalCss = css`
:root {
--color-light: #666;
}
html {
color: #333;
font-family: sans-serif;
font-size: 16px;
line-height: 1.8;
}
(省略)
`
こんな感じでLayoutコンポーネントのHTMLタグに差し込んだ。
import { globalCss } from "../styles/global";
const Layout: FC = (props) => {
return (
<>
<html class={globalCss}>
(省略)
そのほかのコンポーネントではEmotion CSSを使っていたが、運よくHonoのCSSヘルパーと記法が同じだったためimportするライブラリを変えるだけで済んだ。これはラッキー。
vite.config.tsの設定
Bunだけでもローカル開発は出来るがviteを使うとSSGのビルドと開発サーバーの立ち上げを統合でき、コンポーネントの編集などもホットリロードで即反映されるようになるので設定した。具体的には下記。特に複雑な設定はしなくて良いので便利。
import ssg from "@hono/vite-ssg";
import devServer from "@hono/vite-dev-server";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
ssg(),
devServer({
entry: "src/index.tsx",
}),
],
});
build.tsの作成
いわゆるnext export
相当のことをするための処理をbuild.ts
というファイルに書く。
import { toSSG } from "hono/bun";
import app from "./src/index";
toSSG(app);
これをpackage.json
のscripts
あたりに"build": "bun run ./build.ts"
とでも書いておけば終わり。
GitHub Actionsの設定
GitHub Actionsはissueが作成(記事執筆)されると実行される。そこでは先述のように記事データが作成され、それを元にSSGで静的ファイル諸共生成される。それをGitHub Pagesに配信するということをやっている。
処理の前半部分は特に変える必要なく、変更が必要だったのは主にSSGをするところと、配信するファイルのディレクトリを変えるところ。具体的にはこんな感じの設定に修正した。
(省略)...
- uses: oven-sh/setup-bun@v1
with:
bun-version: "1.0.25"
- run: bun install
- run: bun run build
env:
NODE_ENV: production
VITE_GIALOG_BASE_URL: https://yuheinakasaka.github.io/gialog-diary
VITE_GIALOG_PUBLIC_STATIC_URL: https://yuheinakasaka.github.io/gialog-diary
VITE_BLOG_TITLE: Yuhei Nakasaka's Blog
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: static
主にやったのは下記。
- Node.jsをBunに変えた
-
next export
をbun run build
に変更 - 環境変数名の書き換え
-
publish_dir
をout
からstatic
に変更-
next export
ではout
に吐き出してたがそれがstatic
になった
-
まとめ
これら変更を実装し、あとはリポジトリにpushしたら完了した。そういえばRSS Feedの作成については特に手を入れずにルートを設定しただけでそのまま動いたので楽だった。
Honoに変更したからといって何が嬉しいのか?とかはぶっちゃけ特にないのだけど、このくらいの規模の個人ブログならNext.jsよりもHonoの方がブラックボックスが少なくて小さく作れて良いかもしれない。あとは今はGitHub Pagesで完全に静的ファイルの配信のみだけどCloudflare WorkersとかPagesを使いたいという場合はwrangler対応するだけでいいのでその辺も楽かも。
リポジトリは下記。HonoのSSGを試したい人には参考になるかもしれない。
他なんかあれば@razokuloverで聞いて。
Discussion