🍣

個人ブログをNext.jsのSSGからHonoのSSGに移行した

2024/02/05に公開

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をインストールした。
https://bun.sh/

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の根幹のところになる。

https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props

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ヘルパーを使っている点。
https://hono.dev/getting-started/nodejs#serve-static-files

というのも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ヘルパーがあるのでこれを使った。
https://hono.dev/helpers/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.jsonscriptsあたりに"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 exportbun run buildに変更
  • 環境変数名の書き換え
  • publish_diroutからstaticに変更
    • next exportではoutに吐き出してたがそれがstaticになった

まとめ

これら変更を実装し、あとはリポジトリにpushしたら完了した。そういえばRSS Feedの作成については特に手を入れずにルートを設定しただけでそのまま動いたので楽だった。

Honoに変更したからといって何が嬉しいのか?とかはぶっちゃけ特にないのだけど、このくらいの規模の個人ブログならNext.jsよりもHonoの方がブラックボックスが少なくて小さく作れて良いかもしれない。あとは今はGitHub Pagesで完全に静的ファイルの配信のみだけどCloudflare WorkersとかPagesを使いたいという場合はwrangler対応するだけでいいのでその辺も楽かも。

リポジトリは下記。HonoのSSGを試したい人には参考になるかもしれない。
https://github.com/YuheiNakasaka/gialog-diary

他なんかあれば@razokuloverで聞いて。

リンク

Discussion