memo.nnenn0.com

genbitというRust製の静的サイトジェネレーターを作った

このブログは自作の静的サイトジェネレーターgenbitで生成している。

作った理由は、とにかく軽いブログにしたかったから。自分が作りたいブログは記事を読むためだけのブログで、インタラクションも要らずReactを使う動機が薄かった。それに、配信するのがHTMLと少しの静的ファイルだけなら、コントロールする対象が少ないので、キャッシュのような細かい設定も全部自分で決めやすいと考えた。

最初はAstroも考えたが、AIに聞くと、CSSが別ファイルになったり独自の属性が付いたりして少しサイズがあると言われ、使うのをやめた。

しかし、この記事を書くときに公式ドキュメントを見直すと、AstroはJavaScriptを既定で出力せず(Islands architecture)、CSSも4KB未満ならHTMLに埋め込む(build.inlineStylesheets)ので、このブログくらいの規模ならサイズの差は思っていたほど大きくなかったのかもしれない。

ただ、ブラウザネイティブなHTMLを直接扱いたいからAstroを使わなかったというのもある。

genbitはMarkdownからHTMLを生成するCLI

genbitはRustで書いたCLIだ。Rustにしたのはコンパイラが厳密なのと、ランタイムのいらない単体のバイナリで配れて動作も速いから。好きな言語だから機会があれば基本的にRustを書こうと思っていたのもある。

コマンドは3つしかない。

sh
genbit new my-blog  # サイトの雛形を作る
genbit build        # dist/ にサイトを生成する
genbit dev          # 変更を監視しながらローカルでプレビューする

記事はTOMLのフロントマター付きのMarkdownで書く。

toml
+++
title = "最初の記事"
description = "記事で扱う内容を簡潔に説明します。"
created_at = 2026-09-26 09:00
updated_at = 2026-09-26 09:00
tags = ["rust", "web"]
+++

ここに本文を書きます。

content/entries/hello-world.mdに書いた記事は/entries/hello-worldというURLのページになり、ページの枠はTeraのテンプレートで書く。

1ページを1リクエストで読めるようにした

CSSは別ファイルにせず、各HTMLに埋め込んでいる。全ページ共通のcommon.cssに、テンプレートごとのCSS(記事ページならpage.css)を足して<style>に入れ、HTMLごとminifyする。トップページに記事ページ用のCSSは入らない。

ページの見た目を決めるCSSだけはHTMLと一緒に届いてほしいので、HTMLを1回読み込めばページを表示できるようにした。faviconや記事に載せた画像はHTMLを早く読み始めることには関係ないので、後から別のリクエストで読み込めばいいと考えて別ファイルにしている。

サイト内を回遊してもらうならCSSを別ファイルにしてブラウザにキャッシュさせた方がいいのかなとも思ったけど、とりあえず最初に開いたページの読み込みを速くする方向にした。

JavaScriptも使っていない。genbitが出力するHTMLには実行されるJavaScriptが一切なく、<script>は検索エンジン向けの構造化データ(JSON-LD)だけになっている。配信時にはCloudflareのWeb Analyticsが計測用のbeacon.min.jsを差し込んでいるので、ブラウザで実行されるのはそれだけになる。genbit自体がJavaScriptを使うのはgenbit devで保存時にブラウザを再読み込みするためのスクリプトだけで、これはgenbit buildの出力には入らない。

JavaScriptを書くのは避けて、できることはCSSでなんとかするようにしている。ダークテーマもJavaScriptで切り替えず、CSSのprefers-color-schemeでOSの設定に合わせている。SPAっぽいぬるっとしたページ遷移も、CSSの@view-transitionで賄えたらいいなと思って設定している。

シンタックスハイライトと画像の最適化は入れていない。シンタックスハイライトはどうやってもサイズが大きくなりそうだったので、コードブロックは色なしで表示している。画像はgenbitの外で縮小してから載せるようにしている。

Cloudflare Workersで配信している

生成したdist/はCloudflare WorkersのStatic Assetsで配信している。genbitがするのはminifyまでで、転送時の圧縮はCloudflareに任せている。Cloudflareはブラウザが送るAccept-Encodingに合わせて、HTMLやCSSをGzip・Brotli・Zstandardのどれかで圧縮して返す(Content compression)。

この記事を書いている時点で公開中のページをcurlで測ると、転送サイズは次のようになった。単位はバイト。

ページ圧縮なしZstandardBrotliGzip
トップページ2,6721,2261,0981,165
React 19のuseについて書いた記事24,5826,6626,2236,023

どの方式でも、転送サイズは圧縮なしのHTMLに比べてトップページで半分以下、React 19の記事で4分の1ほどになる。

トップページをLighthouseで計測すると、まだ記事が少なくページの文字数も少ないというのもあるが満点だった。

Lighthouseでmemo.nnenn0.comのトップページを計測した結果。Performance、Accessibility、Best Practices、SEOがすべて100

キャッシュの設定もファイルごとに決めている。Workers Static Assetsは既定でCache-Control: public, max-age=0, must-revalidateを返すので、ブラウザはキャッシュを使う前に毎回更新を確認する(Headers)。このブログでは_headersファイルで、記事ページはブラウザで1時間、faviconは1日キャッシュするようにしている。

text
/entries/*
  Cache-Control: public, max-age=3600

/assets/img/favicon.svg
  Cache-Control: public, max-age=86400

/assets/img/favicon.png
  Cache-Control: public, max-age=86400

トップページやRSSフィードは既定のままなので、記事を追加すれば次に開いたときに反映される。

小さいツールでも考えることは多かった

出力するのは転送サイズが数KBのHTMLだけだが、安心して使える形にするまでに考えることは意外と多かった。

例えば、ビルドに失敗したときのdist/の扱い。記事やテンプレートの誤りでビルドが途中で止まったとき、dist/に書きかけのファイルが残ると、それをそのままデプロイしてしまうかもしれない。そこでgenbitは、サイト直下の一時ディレクトリに全ファイルを書き終えてからdist/を入れ替える。ビルドに失敗した場合、前のdist/はそのまま残る。

入れ替えは、古いdist/をバックアップ用のディレクトリへ移してから、一時ディレクトリをdist/にリネームする2段階で行う。2回のリネームは1つのアトミックな操作ではないので、間で強制終了されるとdist/がなくなることがあるが、その場合も古い出力はバックアップのディレクトリに残る。

消してはいけないディレクトリを消さないことにも気を付けた。genbitが作っていないdist/をビルドで入れ替えると、中身を消してしまう。genbitは出力に.genbit-outputというマーカーファイルを置き、マーカーのあるdist/だけを入れ替える。このあたりの挙動はgenbitのドキュメント(dist/の保護)にまとめている。

ほかにも次のようなことを実装している。

Rustのコードはテストを含めて5,300行ほどになり、そのうち半分以上はテストだった。

genbitのロゴ

genbitのロゴも自分で作った。ロゴをどう作ればいいのかわからなくて意外と悩んだ。

genbitという名前はgenerateのgenとbitから取っていて、とても小さいものを生成するという意味で付けた。ロゴはそこから考えて、下に並べた4本の線のうち一番右だけを立たせ、1bitを表すようにした。

genbitのロゴ。暗い正方形の下部に4本の短い線が並び、右端の1本だけが縦に立っている。右側にgenbitの文字

色も背景と線の2色だけにしてサイズを抑えていて、このブログのfaviconにも使っているSVGは190バイトになっている。

まとめ

genbitはコーディングエージェントと一緒に作り、最初のコミットから最初のバイナリをリリースするまで約5日だった。dist/の扱いのような細かい挙動を1つずつ決めて実装し、テストで確かめる作業を自分だけでやっていたら、何倍も時間がかかっていたと思う。

HTMLを出力するだけの小さいツールのつもりで作り始めたけど、予想していなかったポイントがいろいろあって面白かった。これからもこのブログでgenbitを使いながら、必要になったところを直して長く使っていきたい。