kikuta@dev:~$cat ./notes/spex-store-images-as-code.md
·16 min·kikuta

Chrome Web Store用画像をproduction buildから自動生成する

Playwrightで実Google Sheetsを撮影し、RemotionでEN/JAのStore画像をproduction buildから再生成するpipeline。

Chrome拡張 Spex の公開準備で、Chrome Web Store用のスクリーンショットを作りました。

最初は普通に考えていました。

  1. Google Sheetsを開く
  2. Spexを動かす
  3. スクリーンショットを撮る
  4. Figmaや画像編集でタイトルを載せる
  5. 5枚できたら終わり

ところが、実際にリリース直前までプロダクトを触り続けると、この方法がかなりつらいことに気づきました。

KeyTipsの見た目を変える。

キーマップの既定を変える。

画面上の配置を直す。

日本語対応を入れる。

ダークモードやチュートリアルを追加する。

すると、昨日作ったStore画像がもう古い。

しかもChrome Web Storeでは、単に綺麗な画像を作ればよいわけではありません。

現在公開するbuildと、画像の中で見せている挙動が一致している必要があります。

そこで、Store画像を「デザイン成果物」ではなく、production buildから再生成できるbuild artifactとして扱うことにしました。

現在のSpexでは、次のコマンドでStore素材を作り直せます。

pnpm store:assets

この記事では、なぜ静止画5枚のためにPlaywrightとRemotionまで使うことになったのかを書きます。

最初に壊れたのは「スクリーンショットは一度作れば終わり」という前提だった

Git履歴を見ると、Store素材は一度で完成していません。

最初にrelease graphicsを用意した後も、

  • Store iconのsafe area修正
  • primary screenshotのcrop修正
  • 実際の5枚構成の作り直し
  • 英語 / 日本語の両対応
  • trust claimの検証

が続きました。

特にスクリーンショットのcrop修正は象徴的でした。

プロダクトの画面をそのまま全部見せれば「正直」ではありますが、Storeの一覧や詳細ページで見ると、小さすぎて何を見せたいのか分からなくなります。

一方で、見せたい部分だけを手で切り取って作り込むと、次のUI変更でまた撮り直しです。

この時点で、Store画像をPSDやFigmaの完成データとして持つより、どう生成するかをコードで持った方がよいと考えました。

まず、実プロダクトを自動で撮る

SpexのStore画像では、マーケティング用に作った偽のGoogle Sheets画面を使いません。

現在のproduction extensionをbuildし、PlaywrightでChromiumへ読み込ませ、実Google Sheetsを開きます。

概念的には次の流れです。

current Spex source
    ↓
WXT production build
    ↓
.output/chrome-mv3
    ↓
Playwright launches Chromium with the extension
    ↓
real Google Sheets
    ↓
actual Spex state / actual keyboard sequence
    ↓
raw capture

重要なのは、Store画像制作のための別実装を作らないことです。

KeyTipsを見せたいなら、本物のSpexでAltを押します。

Sheets準拠キーマップを見せたいなら、実際にsettingsをそのprofileへ変更します。

コマンド実行前後を見せたいなら、本物のcommandを走らせます。

これにより、Store画像制作のコードがproduction behaviorの第二実装になることを避けています。

撮影用のSheetも自動で作る

実Google Sheetsを使うと、次の問題が出ます。

毎回同じ見た目のfixtureをどう用意するか。

人間が撮影前に、

  • 適当な表を作る
  • 太字にする
  • 選択範囲を合わせる
  • キーマップを変える
  • 余計なToastを閉じる

とやっていると、再現性がありません。

そこでcapture script自身が一時Sheetを作ります。

例えばStore画像のfixtureでは、概念的に次のような表を自動入力します。

Month | Region | Sales   | Cost    | Margin
Apr   | East   | 128,400 | 91,200  | 29.0%
May   | West   | 146,800 | 98,500  | 32.9%
Jun   | East   | 139,600 | 92,400  | 33.8%
Jul   | North  | 158,300 | 103,100 | 34.9%

その上で、

  • headerを太字化
  • 指定セルへ移動
  • Spex profile変更
  • Altを押してKeyTips表示
  • 必要ならcommand実行

までPlaywrightが行います。

「撮影用データを消す」コードの方が怖い

実Google Sheetsへ一時Sheetを作る以上、cleanupが必要です。

ここはかなり保守的にしています。

撮影scriptは、名前が明示的なprefixを持つSheetだけを削除します。

SPEX_PROMO_IMAGE_...
SPEX_PROMO_STORE_...

想定外のSheet名なら削除を拒否します。

Sheet数についても、

baseline
    ↓
+1だけ増えた
    ↓
撮影用prefixである
    ↓
削除

という条件にしています。

Marketing automationで本物のSpreadsheetを触る以上、画像生成の便利さより、誤ってユーザー資産を削除しないことの方が重要です。

EN / JAは「翻訳した画像」ではなく、それぞれ実際に撮る

Spexは日本語・英語に対応しています。

Store画像も両方必要です。

ここで一枚の英語スクリーンショットに日本語captionだけ載せ替える方法もあります。

ただ、それではGoogle Sheets側の表示言語とSpex側のlocaleが一致しません。

そのためcapture段階から分けます。

en
  browser locale: en-US
  Sheets: ?hl=en
  Spex locale: en

ja
  browser locale: ja-JP
  Sheets: ?hl=ja
  Spex locale: ja

同じcapture scenarioをlocaleごとに実行し、その上からそれぞれのcopyを組みます。

これにより、

  • ENだけ古いUI
  • JAだけ別のKeyMap
  • Google Sheetsは英語なのにSpexだけ日本語

のようなStore上の不整合を減らせます。

静止画なのにRemotionを使う

撮ったraw screenshotをそのままStoreへ出すわけではありません。

Store画像には、

  • title
  • short body
  • framing
  • before / after
  • Key sequence
  • product positioning

などを載せます。

ここで画像編集ソフトではなくRemotionを使っています。

動画のためのライブラリという印象が強いですが、RemotionはReact componentから静止画をrenderする用途でも便利です。

Spexでは概念的に、

raw real-product capture
        +
locale-specific copy
        +
shared visual primitives
        ↓
Remotion still composition
        ↓
1280x800 PNG

としています。

5枚の構成は、例えば次のように分けています。

01 Hero
02 Interaction
03 Toolbar
04 Excel shortcuts
05 Why Spex

ENとJAでcomponent structureは共通です。

文章だけをdataとして分離しています。

hero: {
  title: "Press Alt. See what to press next.",
  body: "Spex puts the next key directly on Google Sheets."
}
hero: {
  title: "Altを押す。次のキーが見える。",
  body: "Google Sheetsの上に、次に押すキーをそのまま表示します。"
}

この方法だと、余白やカード位置を修正したとき、EN/JAを別々に直す必要がありません。

Store画像のcopyまで検証対象にした

画像生成を自動化しても、内容が嘘なら意味がありません。

そこでbuild pipelineでは、Remotion renderingの前にtrust claimの検査を入れています。

例えばSpexでは、Privacyについて、

No Spex analytics backend. Settings and diagnostics stay local.

という表現をStore素材で使っています。

こうしたclaimは、実装が変わったのに画像だけ古いまま残ると危険です。

そのため、Store asset buildはproduction sourceと公開claimの整合性を確認するrelease checkと組み合わせています。

つまり画像は単なるPNGではなく、release declarationの一部として扱っています。

最後に寸法も機械で見る

生成後にはffprobeで画像streamを検査します。

期待値と違えば失敗です。

Store screenshots: 1280 x 800
Small Promo:        440 x 280
Marquee:            1400 x 560

さらにQA用に半サイズpreviewとcontact sheetも生成します。

なぜcontact sheetを作るかというと、5枚を一枚ずつ開いていると、シリーズ全体のバランスが見えにくいからです。

EN 5枚 -> en-contact.png
JA 5枚 -> ja-contact.png

最終確認では、

  • 5枚の情報密度が偏っていないか
  • 1枚だけ文字が小さくないか
  • EN/JAで構図がズレていないか
  • 同じ画面を何度も見せていないか

を一覧で見ます。

現在の生成フロー

最終的には次の6段階になりました。

1. production Spex build
2. real Google Sheets capture
3. prepare image assets
4. validate claims / ESLint / TypeScript
5. Remotion still render
6. dimensions / QA previews / contact sheets

コード上もほぼこのままログを出します。

[store-image] 1/6 build production Spex
[store-image] 2/6 capture current real Google Sheets states
[store-image] 3/6 prepare image assets
[store-image] 4/6 validate claims and Remotion source
[store-image] 5/6 render Store stills
[store-image] 6/6 verify and create QA previews

何が一番良かったか

自動化したことで制作時間がゼロになった、という話ではありません。

むしろStore画像の構成やcopyは何度も人間がレビューします。

良かったのは、「この画像は今の製品を写しているのか?」を毎回疑わなくてよくなったことです。

手作業だと、綺麗に作った画像ほど更新しづらくなります。

コード生成なら、UIを変えたときは撮り直す前提です。

そして変更後に、

pnpm store:assets

を実行すれば、現在のproduction buildから一式を作り直せます。

Chrome拡張に限らず、StoreやApp Marketplaceへ継続的に画像を出すプロダクトでは、Marketing assetもrelease artifactとして扱う価値があると思います。

Spex

この記事で扱ったpipelineは、Google Sheetsをキーボード中心で操作するChrome拡張SpexのStore素材制作に使っています。