SSGの「面倒くさい」を消し去る
Astroなどの静的サイトジェネレーター(SSG)は、パフォーマンスと自由度は最高ですが、「画像の扱い」に関しては独特の作法(public/フォルダへの移動やパスの書き換えなど)を要求されることが多く、執筆リズムを崩しがちです。
「画像をドラッグ&ドロップして終わり」という、執筆において最も直感的で摩擦のない体験をローカル環境で再現したい。
そのために私が開発したのが、このプロジェクト専用の運用ツール manager.py です。
今回は、このスクリプトが裏で何をしているのか、その「泥臭い」内部ロジックを解説します。
1. コマンド一発でPublish
このスクリプトの最大の役割は、執筆用のディレクトリ(projects/)にあるMarkdownファイルを、Astroのビルドディレクトリ(web/src/content/blog/)へ変換・転送することです。
# 魔法のコマンド
uv run python web/scripts/manager.py publish projects/.../article.md --slug my-article
このコマンドを叩くだけで、以下の処理が一瞬で走ります。
- Lint: 日本語のチェック(AI特有の不自然な接続詞等の禁止)。
- Image Upload: 記事内のローカル画像を正規表現で検出し、Cloudflare R2へアップロード。
- Link Replacement: 画像パスをR2の公開URL (
https://r2.a3ro.cc/...) に置換。 - Frontmatter Update: 記事のヘッダー情報(日付やアイキャッチ画像)を整備。
- Copy: 最終的なMarkdownをAstro側へ配置。
2. 正規表現による画像パイプライン
最も重要なのが画像の処理です。
Markdown内の画像リンクは、執筆時点では相対パス (./image.png) になっています。これを本番用のURLに書き換える必要があります。
ここで活躍するのが、古典的ですが強力な 正規表現 (Regex) です。
# 画像記法  を検出する正規表現
image_pattern = re.compile(r'!\[(.*?)\]\((.*?)\)')
def replace_image(match):
alt_text = match.group(1)
local_path = match.group(2)
# 1. ローカル画像をCloudflare R2へアップロード
public_url = upload_to_r2(local_path)
# 2. R2のURLに置換して返す
return f''
# 全文を一括置換
new_content = image_pattern.sub(replace_image, content)
AST(抽象構文木)パーサーなどを使わず、あえて正規表現で文字列置換を行うことで、高速かつシンプルに実装しています。ブログ記事程度の複雑さなら、これで十分なのです。
3. 「ヒーロー画像」の自動抽出
ブログ一覧で表示されるサムネイル(ヒーロー画像)の設定も自動化しています。
記事内で最初に登場した画像を自動的にヒーロー画像として認識し、Frontmatterの heroImage フィールドにURLをセットします。
# 最初の画像が見つかったら、Frontmatterを更新
if first_image_url:
hero_pattern = re.compile(r'^heroImage:\s*.*$', re.MULTILINE)
new_content = hero_pattern.sub(f'heroImage: "{first_image_url}"', new_content)
このおかげで、「記事を書く」→「画像を貼る」→「コマンド実行」だけで、アイキャッチ付きの記事が完成します。
4. ツールに使われるな、ツールを作れ
世の中には便利なCMSやヘッドレスCMSが溢れています。 しかし、それらに合わせるのではなく、「自分の理想の執筆フロー」に合わせて道具を作ることこそが、エンジニアリングの醍醐味であり、長期的な生産性を高める鍵だと私は考えています。
この manager.py たった1ファイルのPythonスクリプトが、私の執筆活動のすべてのストレスを取り除いてくれました。