← ブログ一覧

サイトを大改造した話——ダーク海洋テーマからブログ機能フル装備まで

TOPページをジンベエザメの深海テーマに刷新し、タグ・検索・読了時間・OGP自動生成など10以上のブログ機能をClaude Codeで一気に実装した記録


登場人物

  • リナ社長 … 高校生なのに会社経営するやり手ギャル。テックにも強くてAI活用が得意。
  • タクヤ … 入社3年目の男性社員。真面目で少しだけコードが書ける。

きっかけ——「なんか地味じゃない?」

タクヤがいつものようにPCに向かっていると、リナ社長がモニターを覗き込んできた。

リナ社長「ねえタクヤ、jinbei-labのTOPページってさ、なんか地味じゃない?ジンベエザメのサイトなのに全然ジンベエザメ感ないし」

タクヤ「デフォルトのAstroテーマそのままですからね……確かに」

リナ社長「やばくない?せっかく jinbei-lab って名前なのにさ。深海感とか、実験所っぽい雰囲気とか、もっと出せるじゃん」

そこからリナ社長の「サイト大改造計画」が始まった。


TOPページをジンベエザメの深海へ

まずTOPページのデザインを根本から変えた。キーワードは「深海の実験所」。

  • 背景色を #061325(深い海軍色)に
  • ジンベエザメの画像(Geminiで生成)をヒーローに配置
  • 青緑の発光色(#00d4aa)をアクセントカラーに
  • 気泡が浮かぶCSSアニメーション

リナ社長「わ、めっちゃいい!本物の実験所みたい!」

タクヤ「気泡アニメーションがじわじわ動いてて、確かに深海感ありますね」

ポイントはCSSのスコープ管理。他のページに影響が出ないよう、body.lab-home-page というクラスで完全に分離している。ブログ一覧(.blog-list-page)や記事ページ(.blog-post-page)も同じ作戦で、ページごとにテーマカラーを持たせた。


ブログ一覧・記事ページも全面リニューアル

TOPが良くなったら、ブログ周りも気になってきた。

リナ社長「ブログ一覧、どの記事がどれかわかりにくいんだけど。カードにもっと情報入れてよ」

リナ社長のひとことで、ブログ周りの改修リストが膨らんだ。

追加した機能一覧

機能内容
日付降順ソート新しい記事が上に来るように
読了時間一覧カードと記事ページ両方に表示
タグ表示一覧カードにタグピルを表示
サブ記事の差別化sub タグ付きはオレンジ系に変色
検索バータイトル・説明・タグをリアルタイム検索
カレンダーサイドバーに表示、日付で絞り込み
前後記事ナビ記事下部に前の記事・次の記事ボタン
OGP画像自動生成SNSシェア時にタイトル・タグ入りの画像を表示
コメント機能Giscus(GitHub Discussions連携)で導入

リナ社長「これだけ詰め込んだのに1日で終わったじゃん。Claude Codeすごくない?」

タクヤ「コード量は結構多かったんですが、設計を指示すれば実装してくれるので……確かに早かったです」


読了時間の計算ロジック

タクヤ「読了時間ってどうやって計算してるの?」

リナ社長「日本語と英語で読む速さが違うから、それぞれ別に計算してるよ」

export function calcReadingTime(body: string): number {
  const japanese = (body.match(/[\u3000-\u303F\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/g) || []).length;
  const english  = (body.replace(/[\u3000-\u303F\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/g, ' ')
                       .match(/\b[a-zA-Z0-9]+\b/g) || []).length;
  return Math.max(1, Math.ceil(japanese / 400 + english / 200));
}

日本語は400文字/分、英語は200単語/分で計算。タクヤがこの記事の読了時間を確認したら「約3分」と出た。

タクヤ「ちゃんと機能してる」


OGP画像でつまずいた話

ここが一番ハマった。

OGP画像(SNSシェア時に表示されるサムネ)を自動生成するため、Satoriというライブラリを使った。最初は src/pages/og/[slug].png.ts というAstroのAPIエンドポイントとして実装したのだが……。

リナ社長「Cloudflareでビルドしたら Error: No such module "node:fs" って出たんだけど」

ローカルでは動くのに、Cloudflare Workersのランタイムには node:fs がない。フォントファイルの読み込みに使っていたのが原因だ。

解決策:Astroページとして書くのをやめて、ビルド前に実行するNode.jsスクリプトに切り出した。

"build": "node scripts/generate-og.mjs && astro build"

scripts/generate-og.mjs がビルド前にMarkdownを全部読み込んでPNGを生成、public/og/ に保存。Astroはそれを静的ファイルとして配信するだけ。Node.jsコンテキストで動かすのでWorkers制約に引っかからない。

タクヤ「分離すれば良かったんだ」


予約投稿もできるようになった

リナ社長「この記事、明日の9時に公開したいんだけど、自動でできる?」

できる。静的サイトでの予約投稿は:

  1. pubDate を未来の日時に設定してコミット
  2. ブログ一覧・記事生成時に「現在より未来のpubDateは表示しない」フィルターを追加
  3. GitHub Actionsで毎日9時(JST)に空コミット→pushしてCloudflare Pagesの再ビルドをトリガー
on:
  schedule:
    - cron: '0 0 * * *'  # 毎日 0:00 UTC = 9:00 JST

ビルド時刻が pubDate を超えた瞬間から記事が公開される仕組み。完全無料(GitHub Actionsはパブリックリポジトリ無料、Cloudflare Pagesも無料枠内)。

リナ社長「無料でここまでできるの、ふつうにすごいわ」


まとめ

今回の改修でやったこと:

  • デザイン刷新: 深海・実験所テーマでブランドらしさを出した
  • ブログ機能: タグ・検索・読了時間・OGP・前後ナビなどを一気に実装
  • インフラ: Cloudflare Workersの制約を理解した上でビルドフローを設計
  • 予約投稿: GitHub Actions × Cloudflare Pagesで完全自動化

リナ社長「全部Claude Codeにやらせたじゃん」

タクヤ「指示出しと確認はしてますよ……」

リナ社長「それがエンジニアリングだよ」

コメント