このサイトは両端がRubyでできている。markdownをデータに変えるのがビルド側のRuby(CRuby)、そのデータからページを描くのがエッジのRuby(mruby/edge + Funicular)、ブラウザで続きを引き取るのもRuby(PicoRuby)。このページはその運用ガイド。
HTMLはビルドでは作らない。 rake buildが出すのは記事のデータ(/data/*.json)とフィードとアセットで、ページはCloudflare Workerが受け取ったときに組み立てる。だから記事を1本足すのに焼き直すのはデータだけで、Workerのwasmもブラウザのapp.mrbも変わらない。
日常のコマンド
bundle exec rake test # テスト(test-unit)
bundle exec rake build # 既定development: content-dev/→tmp/development/public/にデータとアセットを生成(本番は下記参照)
bundle exec rake homepage:serve # ビルドしてWorkerごと起こす(http://localhost:8787)
環境はBASH0C7_HOMEPAGE_ENVで切り替える(未指定はdevelopment)。development(既定)はcontent-dev/(検証用fixture)を読み、tmp/development/public/へ書き出す。本番ビルド・デプロイはBASH0C7_HOMEPAGE_ENV=production bundle exec rake buildでcontent/を読み、public/へ書き出す。new:post/new:episode/transcribe:episode/admin:build/admin:serveも同じ切り替えに従う。
記事を書く
bundle exec rake new:post
実行時刻でファイル名(<content>/posts/YYYY-MM-DD-HHMMSS.md)が自動採番され、draft: true付きで生成される。<content>は既定developmentではcontent-dev/、BASH0C7_HOMEPAGE_ENV=production指定時はcontent/。タイトルを考える必要はなく、frontmatterのtitleは空のままでもよい。空の場合、一覧やページ見出しには日時(2026-08-21 15:30の形)が表示される。本文を書き、公開するときにdraft行を消す(draftのままだとビルド対象外なので、書きかけをpushしても公開されない)。
記事のfrontmatter:
| キー | 必須 | 説明 |
|---|---|---|
title |
- | 記事タイトル。空なら一覧・ページ見出しは日時(2026-08-21 14:30の形)にフォールバックする |
date |
○ | 2026-08-21または時刻つき2026-08-21 14:30:00 +09:00。RSSのpubDateに反映される |
description |
- | OGP/meta descriptionに使う要約。無ければサイト説明で代用 |
image |
- | カバー画像(og:image、ページ上部にも表示)。無ければページ上部のカバーはsite.ymlのdefault_cover_image、og:imageは/og(Cloudflare Worker)がタイトル入りで動的生成する |
draft |
- | trueでビルド対象外 |
unlisted |
- | 固定ページでtrueにすると、一覧・sitemap・検索(noindex)に載らない。URLを知る人だけが開く(/pay.htmlなど) |
本文はmarkdown(GFM)。コードブロックは```langでシンタックスハイライトされる。画像は<content>/images/に置くと/images/へコピーされるのでで参照する。音声/動画は<content>/media/に置くと/media/へコピーされる。
エピソード(podcast)を出す
bundle exec rake new:episode
# 音声/動画ファイルを渡すとlength/durationを自動算出:
bundle exec rake "new:episode[/path/to/audio.mp3]"
<content>/episodes/NNN-YYYY-MM-DD-HHMMSS.mdが採番されて生成される(NNNはitunes:episode番号、ファイル名の残りはタイムスタンプ)。<content>は記事と同じく既定developmentではcontent-dev/、本番はBASH0C7_HOMEPAGE_ENV=production指定時のcontent/。音声ファイルを渡すとaudio_length(バイト数)とduration(afinfoまたはffprobeによる再生時間)が自動で埋まる。audio_urlは音声を実際に配置したURLに書き換えること。
エピソードのfrontmatter(記事のキーに加えて):
| キー | 必須 | 説明 |
|---|---|---|
audio_url |
○ | 音声/動画ファイルの配信URL |
audio_length |
○ | ファイルのバイト数(enclosure length) |
duration |
○ | 再生時間"HH:MM:SS" |
media_type |
- | 既定audio/mpeg。videocastはvideo/mp4(ページのプレーヤーもvideoに切り替わる) |
episode |
- | itunes:episodeのエピソード番号 |
podcastの録音・ホスティング自体はこのSSGの範囲外。このサイトが保証するのは「audio_urlを書けばプレーヤーが出る」ところまで。ただし音声ファイルを渡した場合、ffmpegによる簡易編集(無音トリム+ラウドネス正規化)と、gems/rb-speech-mac(Apple Speech.frameworkのSpeechAnalyzer/SpeechTranscriberをswift_gem経由でラップしたオンデバイス音声認識のsubgem)による文字起こしの下書きが自動で走る。ネットワークやAPIキーは不要だが、macOS 26(Tahoe)以降・Apple Silicon・Swift 6.3+が必須で、既定ではbundle installに含まれない(bundle install --with transcriptionで有効化)。日本語は既定ロケールja_JPで扱う。ffmpeg/rb-speech-macが無い/失敗した場合はそれぞれ元ファイルのまま・文字起こし無しにフォールバックする。ffmpegはMac側に別途インストールしておくこと。audio_urlは音声を実際に配置したURLに書き換えること。文字起こしは## 文字起こし見出し配下に自動挿入されるので、内容を確認・修正してから公開する。
rb-speech-macが使えない環境(Mac以外・Claude Code webでの作業・Mac不調時など)でも縮退運転できる。 new:episodeはffmpeg/rb-speech-macのどちらが無くても(あるいは両方無くても)止まらずにエピソードを生成し、文字起こしを省略した場合は次のように後から当てるコマンドを表示する。
bundle exec rake "transcribe:episode[NNN-YYYY-MM-DD-HHMMSS,/path/to/audio]"
rb-speech-macが使える環境(実機Mac)でこれを実行すると、指定した音声ファイルを文字起こしして該当エピソードの## 文字起こし見出しに追記する。transcribe:episodeも既定developmentではcontent-dev/を書き換え、本番のエピソードに当てるにはBASH0C7_HOMEPAGE_ENV=productionが必要。元の音声/.processed.mp3さえ残しておけば、生成そのものはどの環境で行っても後から文字起こしだけ差し替えられる。既に## 文字起こしがあるエピソードに対しては何もせず終了する(二重挿入防止)。
録音・編集の具体的な手順
このSSGが自動化するのは音声ファイルを渡した後(簡易編集・文字起こし・スタブ生成)から。録音そのものはMac標準のQuickTime Playerで十分、追加インストールは要らない。
- 録音する(QuickTime Player) — 音声だけなら「ファイル→新規オーディオ収録」、画面や顔も残すなら「新規スクリーン収録」/「新規ムービー収録」。終わったら「ファイル→書き出す」でm4a/movとして保存する
-
取り込む
bundle exec rake "new:episode[/path/to/raw.m4a]"ffmpegでの無音カット・音量そろえと、rb-speech-mac(導入済みの場合)での文字起こしが自動で走り、
<content>/episodes/NNN-....mdができる(既定developmentではcontent-dev/へ) - 仕上げる — 元ファイルと同じ場所にできる
.processed.mp3を実際のホスティング先にアップロードしてaudio_urlを書き換え、自動挿入された## 文字起こしを読んで誤字や聞き違いを直し、title/descriptionを書いてdraft行を消す
QuickTime Player以外(Audio Hijack、Descript等)を使ってもよい。このSSG側が必要とするのは、最終的に手元に音声/動画ファイルが1つある状態だけ。
Claude Code webなどrb-speech-macが使えない環境で2の取り込みを行った場合、文字起こしは省略されコマンド例が表示される。.processed.mp3(または元音声)を残しておき、実機Macでrake "transcribe:episode[...]"を後から実行すればよい。
管理画面(発令所)で原稿を見る
記事の原稿を書く・直す・公開するのはGitHubで行う(iPhoneのアプリでもよい)。管理画面は公開サイトと同じWorkerに同居する/katteguchi-gateで、読むだけ。ローカルでは次で動く。
bundle exec rake admin:serve # ビルドしてWorkerごと起こす。http://localhost:8787/katteguchi-gate
bundle exec rake status # 公開準備状況をCLIで表示(要対応があれば非0で終了。公開前チェック用)
ローカルの門はADMIN_AUTH=local(Hostがループバックのときだけ)。原稿の一覧はworkers/.dev.varsにGITHUB_TOKEN(このrepoのIssuesを読めるfine-grained PAT)を置くと出る。本番の門はCloudflare AccessとADMIN_EMAILS(手順はdocs/edge-admin.md)。
発令所は公開サイトと同じFunicular + PicoRuby.wasmのSPAで、バックエンドはWorkerの中のSinatra(app/edge/admin_app.rb)。three.jsの主画面の上に窓が浮く発令所のメタファーで、文字は16.5pt基準。
- MANUSCRIPTS — GitHubの
原稿ラベルのissueの一覧。行を押すとPREVIEWが開く。「GitHubで新しく書く」から、型ごとのissueのテンプレートを開ける - PREVIEW — 公開サイトと同じ見え方で原稿を描いたもの(本文のHTMLはGitHubのMarkdown API)
- WEBMENTIONS — 受け取ったWebmention(D1)
- HEARTS — 記事ごとの❤️の累計と直近14日の推移(D1。押されていない記事は0で並ぶ)
検出できるのはcontentのファイルから機械的にわかることだけ(rake status)。記事ごとには、audio_urlがexample.com等のダミーのまま、audio_length/durationが雛形のままかテスト用の極端に小さい値、本文に## 文字起こし見出しが無い、ダミー値のままdraftが外れている、の4種類。サイト全体ではpodcast-art.pngの未配置。
固定ページを増やす
<content>/pages/slug.mdを置くと/slug.htmlに生成される(このページ自身もcontent/pages/help.md、<content>は既定developmentではcontent-dev/)。frontmatterはtitle必須、description/draft/unlistedは任意。
ホームの構成
ホーム(/)は上から次の3段。
- hero —
site.ymlのauthorを見出しに、taglineを一言添える。hero_imageはホームのog:image(SNSでの共有カード用)として使われる - Article —
content/postsとcontent/episodesを日時でマージした一覧。ブログ記事とpodcastエピソードが時系列で混在する - Profile —
content/profile.mdの本文
フィードの考え方
/feed.xml— ブログのRSS。最新20件(site.ymlのfeed_limit)を全文込みで配信する。RSSは「通知の窓」でありアーカイブはこのサイト本体、という役割分担/podcast.xml— podcastのRSS 2.0 + itunes namespace。Spotify/Apple Podcasts/YouTube(RSS取り込み)にはこのURLを登録する
どちらもブログ/podcastそれぞれ専用のフィードで、ホームのArticleのように混ざったりはしない。
Article一覧はページ送りを持たず、公開済みを全部出す。
サイト全体の設定
content/site.ymlで管理する: サイトタイトル・URL・説明、author、tagline、hero_image、default_cover_image、feed_limit(既定20)、podcastの番組情報(title/description/image/category/owner_email)。
仕組み
lib/page.rbがfrontmatter+markdownを読み、lib/site_data.rbが両端の読むdata/*.jsonを書く。feed.xml・podcast.xml・sitemap.xml・それを参照するrobots.txtはERBのまま(HTMLのERBは1枚も無い)。
ページを描くのはapp/edge/のRuby(PicoRuby、workers/picoruby-cf/が動かす)。ssr.rbが受け取ったJSONをCorpusへ積み、app/funicular/のコンポーネント(ブラウザが載せるのと同じソース)がHTMLを組み、layout.rbが<html>から</html>までを包む。レイアウトもレンダラも1つだけなので、ビルド側とエッジ側で見た目がズレることが起きない。
ブラウザは同じFunicularでhydrateして、以降の遷移を引き取る。遷移した先で足りないデータは/data/から静的アセットとして引く(Workerは呼ばれない)。
CSS(static/style.css)を変えたらrake buildし直すだけ。app/edge/のRubyを変えたときだけnpm --prefix picoruby-cf run buildでwasmを焼き直す。
rake buildは出力先ディレクトリを毎回消してから作り直す(clean build)。出力先に手で置いたファイルは残らないので、配信したいものはstatic/か<content>/images/・<content>/media/に置く。
発令所は同じWorkerの別経路: /katteguchi-gateをapp/edge/admin_app.rb(Sinatra、読むだけ)が受け、画面はapp/admin/のFunicularアプリをadmin.mrbへ焼いたもの(公開サイトのapp.mrbとは別物)。rake buildがkatteguchi-gate/assets/へ置き、deployが公開サイトと一緒に載せる。見た目はtemplates/admin/style.cssとtemplates/admin/js/。