Hugo+Cloudflare pagesを用いたレトロ個人ページの作成


概要
このブログそのものを作った記録。
静的サイトジェネレータにHugoを採用し,テーマは「レトロ個人ページ」の雰囲気を狙って独自に作成した。
古くシンプルな見た目とは裏腹に,内部は次のような構成で成り立っている。
- Cloudflare Pages —— 静的サイトのホスティングと自動デプロイ
- Hugo —— 静的サイトのビルド
- Cloudflare Pages Functions —— アクセスカウンタ・いいね・コメントのAPI
- Cloudflare D1 —— 上記の永続化に使うSQLiteデータベース
- Cloudflare Turnstile —— コメント投稿時のCAPTCHA
- Google Analytics —— アクセス解析
- Open Graph —— SNS共有時のカード表示
ダイアルアップ接続で読めそうな見た目をしているが,実際の転送量は約14 MBある。かなり重い。
コンセプト
90〜00年代の個人サイトの雰囲気を,現代のホスティング基盤の上で再現することを目標にした。
具体的には次のような要素をテーマに盛り込んでいる。
- 1カラム・中央寄せの固定幅レイアウト
- 青系モノクロの配色
- ドットパターンの背景
- チルダ(
~)で装飾したタブメニュー - ホームに表示されるランダム画像とランダムポエム
- 画像クリックで拡大するライトボックス
- ランダムポエムを表示する404ページ
見た目はレトロだが,レイアウトはレスポンシブ対応であり,スマートフォンからでも崩れずに読める。
静的サイトで「動的機能」を実装する
このプロジェクトで一番こだわったのは,静的ページ配信のままアクセスカウンタ・いいね数・コメント欄を実装している点である。
Hugoが吐き出すのはあくまで静的HTMLだが,動的な部分はCloudflare Pages Functions(エッジで動くサーバレス関数)とD1(SQLite)の組み合わせで賄っている。
アクセスカウンタ
サイト全体の通算アクセス数をD1に保持し,アクセスごとにカウントする。
ただし同一IPからの連続アクセスでカウントが膨れ上がらないよう,短い時間のうちの再訪はカウントしない重複排除を入れている。
判定にIPアドレスをそのまま使うことはせず,ハッシュ化した値だけを扱うようにしている。
↓かわいい
https://x.com/rei_512_/status/2021942700996727128
いいね
記事ごとにいいね数をカウントする。
こちらも同じ仕組みで,1記事につき1IPで1回までという制約を設けている。いいねの取り消しにも対応している。
コメント欄
記事ごとのコメントをD1に保存する。
不特定多数が投稿できる以上,スパムと攻撃の対策が必須なので,次の多層防御を入れている。
- Cloudflare Turnstile: 投稿時にCAPTCHA認証を挟む。
- レート制限: 同一IPからの短時間の連投を制限する。
- 入力バリデーション: 名前・本文に必須チェックと文字数上限を設ける。
- XSS対策: 投稿内容はそのまま表示せず無害化してから扱う。
- アクセス元チェック: 当サイト以外からのリクエストを弾く。
OGP対応
SNSで共有されたときのカード表示にも対応している。
記事ページのOGP画像は,明示的に指定があればそれを,なければ本文中の最初の画像を自動で拾って使う。
さらにHugo側で共有用のサイズに整え,オーバーレイ画像を重ねてから書き出している。
逆に,記事中に貼った裸の外部URLは自動でOGPカードに展開される。
リンク先の情報を読みに行ってカードとして描画し,取得に失敗したときは元のリンクをそのまま残すようにしてある。
ブログバナー
レトロ個人サイトらしく,相互リンク用のブログバナーまで作成した。サイズは200x40px。

配布ページはこちら。
https://blog.deltav-lab.org/banner/
執筆フローの自動化
Hugoで記事を書くと執筆環境にかなり依存するのが面倒だったので,Obsidian経由でどのデバイスからでも書けるよう別途パイプラインを整備した。
そちらの設計・実装は別記事にまとめている。
https://blog.deltav-lab.org/projects/obsidianを介した環境非依存のブログ執筆システムの構築/