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

このコマンドを叩くだけで、以下の処理が一瞬で走ります。

  1. Lint: 日本語のチェック(AI特有の不自然な接続詞等の禁止)。
  2. Image Upload: 記事内のローカル画像を正規表現で検出し、Cloudflare R2へアップロード。
  3. Link Replacement: 画像パスをR2の公開URL (https://r2.a3ro.cc/...) に置換。
  4. Frontmatter Update: 記事のヘッダー情報(日付やアイキャッチ画像)を整備。
  5. Copy: 最終的なMarkdownをAstro側へ配置。

2. 正規表現による画像パイプライン

最も重要なのが画像の処理です。 Markdown内の画像リンクは、執筆時点では相対パス (./image.png) になっています。これを本番用のURLに書き換える必要があります。

ここで活躍するのが、古典的ですが強力な 正規表現 (Regex) です。

# 画像記法 ![alt](./path) を検出する正規表現
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'![{alt_text}]({public_url})'

# 全文を一括置換
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スクリプトが、私の執筆活動のすべてのストレスを取り除いてくれました。