Astro から Hono への移行

開発

Astro から Hono へ移行し、記事ごとに設定を変えられる自作 SSG を構築した記録です。

目次

きっかけ

Hono をよく目にするようになった

最近 Hono を応援したいからという理由で個人サイトを Hono の SSG を使って作り直したいといっている人をちらほら見るようになりました。検索してみると、次のように Hono へ移行している先人が結構いました。

見てみると、SSG を自作するうえで時間をかけたくない (作り込みが面倒な) 部分が Hono の SSG によってだいぶ楽になりそうだと感じました。

Astro の SSG にやや不満があった

Astro を触って 1-2 ヶ月しか経っていないので、そもそも知識不足があると思いますが、Astro の SSG は全コンテンツが共有しているという認識です。つまり、たまによく使うプラグインを追加すると、そのプラグインを必要としない記事の編集時にホットリロードがやや重くなる問題がありました。例えば連載記事の場合、そのトピックにだけ必要なプラグインというものがあります。そのプラグインは色々あって多言語や WASM も絡んで結構ヘビーなので、ホットリロード毎に一瞬 PC が唸ります。これが他所にも影響を及ぼすのが気がかりなので、編集する記事によって SSG を変えられるようにできれば良いのにと考えていました。

つくる (記事の書き方)

静的サイトジェネレーターはどれもコンテンツがソースコードの一部なんですが、これが自分には合わなかったです。1 つのディレクトリ配下にすべて押し込む形式は、Web アプリを作るときは別に良いのですが (なので Remix を使っていても気にならない)、コンテンツがメインのサイトで、コンテンツの編集に最も時間を使うというのに、コンテンツがビルドのための何かと同居しているのが少し目障りに感じていました。ちなみに Hono を使うといいながら HonoX を採用しなかった理由です。

自作 SSG ではビルドに必要な何かをすべて src ディレクトリに押し込んで、コンテンツはドメイン名でディレクトリを切ってその中に入れるようにしました。ディレクトリ名がドメイン名なのは、VSCode のウィンドウを目視で探すときに便利だからです。現在は記事を書くときに Markdown と画像ファイルしか目に入ってきません。

記事の内容以外の情報 (タイトルや公開日など) は YAML フロントマターで書くようにしました。これは Astro でもやっていて、そこに不満はなかったのでそのまま引き継ぎです。ただし SSG をコンテンツによって変えるために、最終的に次のような形式にしました。

---
builder:
  name: <Markdown ビルダーの名前> # 設定がなければ名前だけを指定できる
  <key>: <value> # ビルダーの設定
  ...
template:
  name: <Markdown コンテンツのテンプレートの名前> # 設定がなければ名前だけを指定できる
  <key>: <value> # テンプレートの設定
  ...
---

builder は Markdown を HTML にするビルダーの名前で、remark を使って例えば次のように定義します。

builders/textonly.ts
const ParamsSchema = z.object({});

export default createBuilder(ParamsSchema, () =>
  unified()
    .use(remarkParseMarkdown)
    .use(remarkParseGfm)
    .use(remarkParseDirective)
    .use(remarkCalloutDirective)
    .use(remarkTrimSpaces)
    .use(remarkToRehype, { allowDangerousHtml: true })
    .use(rehypeToRaw)
    .use(rehypeExternalLinks, {
      target: "_blank",
      rel: ["noopener", "noreferrer"],
    })
    .use(rehypeStringify, { allowDangerousHtml: true }));

template はビルドされた Markdown (つまり HTML) を埋め込むテンプレートの名前で、hono/jsx を使って最終的に HTML タグで囲われたコンテンツを生成します。

templates/home.tsx
const ParamsSchema = z.object({
  description: z.string(),
});

export default createTemplate(ParamsSchema, c => (
  <Root
    path={c.path}
    title={[]}
    styles={[
      "home.css",
      ...c.styles,
    ]}
    scripts={c.scripts}
    keywords={[]}
    description={c.template.description}
  >
    <main dangerouslySetInnerHTML={{ __html: c.content }} />
  </Root>
));

上記のビルダーとテンプレートを使うと、次のように記事を書くことができます。

tai-kun.dev/index.md
---
builder: textonly
template:
  name: home
  description: 技術系のテーマを中心に、主に著者の備忘録用に運営されているプログサイトです。
---

# 著者

プログラムを書いています。C、C++、Verilog-HDL をよく使っていました。
最近は TypeScript、Rust、Bash、Python (頻度順) です。

投稿された記事の一覧を表示するなど、動的にページを生成する際は、nocontent というビルダーを使い、どのテンプレートをページの生成に使うかを指定します。

tai-kun.dev/bin/index.md
---
builder: nocontent
template:
  name: bin-index
  title: 単発記事の一覧
  description: 単発記事の一覧です。
---

ビルド後のディレクトリ構造を 1 度も気にしたくないので、テンプレートにすべての情報をハードコーディングできる状態であっても、あえてパラメーター等だけはコンテンツの領域に置くようにしています。

やらなかったこと

HonoX を使わない理由は先に述べたとおりですが、hono/css という Hono 用の CSS in JS も使いませんでした。試してみたところ、hono/css によって挿入されたスタイルは Vite のビルドに認識されないようです。ソースコードでは CSS nesting を使って、その機能に未対応のブラウザのためにビルド時に変換しようにもできません。生成された HTML を parse して変換することもできると思いますが、そこまでの手間とビルド時間肥大化の懸念を残してする価値があるのかは疑問です。今回は素直に styles ディレクトリに CSS ファイルを置きました。

使っているプラグイン

remark:

  • @microflash/remark-callout-directives
  • remark-definition-list
  • remark-directive
  • remark-extract-frontmatter
  • remark-flexible-code-titles
  • remark-frontmatter
  • remark-gfm
  • remark-link-card
  • remark-parse
  • remark-rehype
  • その他自作のプラグイン

rehype:

  • rehype-expressive-code
  • rehype-external-links
  • rehype-raw
  • rehype-stringify

便利なライブラリ

  • pagefind
  • rss
  • sharp
  • vite-plugin-sitemap