hz-store-pwa について
hz-store-pwaは、WebアプリをMeta VRアプリとしてパッケージ化し、Meta Horizon Storeへ提出する手順を案内するスキルです。2Dパネルと没入型WebXRの両方を対象に、デプロイ、PWA設定、APK作成、アップロードを扱います。
主な機能
- 012Dパネルと没入型WebXRのモード案内
- 02VercelデプロイとPWAマニフェスト設定
- 03@meta-quest/bubblewrap-cliによるAPK作成
- 04署名、Digital Asset Links、ストア提出の案内
活用シーン
- 01Meta Horizon Store向けWebアプリの準備
- 022Dと没入型のアプリモード選択
- 03デプロイ済みPWAの署名付きAPK化
- 04TWAの起動やストアアップロードの問題調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ meta-quest/ agentic-tools --skill hz-store-pwaインストール後、Claude Code で/hz-store-pwaを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/app-modes.mdbubblewrap-packaging.mdmanifest-and-icons.mdstore-upload.mdtroubleshooting.mdvercel-deploy.md詳細
Store PWA/TWA スキル
Web アプリを Meta VR アプリとしてラップし、Meta Horizon Store に公開するまでのエンドツーエンドの手順を案内します。このスキルでは、2D ウィンドウパネルと没入型 WebXR/VRの両方の配信モードを、同じパイプラインで扱います。Web アプリのビルド、Vercel へのデプロイ、PWA マニフェストとアイコンの追加、@meta-quest/bubblewrap-cli による署名済み Meta VR APK のパッケージ化、ovr-platform-util によるアップロードまでを網羅しています。
コマンドでは <…> トークン(例:<DOMAIN>、<HORIZON_APP_ID>、<team-slug>、<PW>)を使います。実行前に、ご自身の値に置き換えてください。
このスキルを使う場面
次のような場合に、このスキルを使ってください。
- Web アプリ(2D または WebXR)を PWA/TWA として Meta Horizon Store に公開する
- アプリを 2D パネルとして実行するか、没入型 WebXR セッションとして実行するかを決める
- IWSDK で WebXR アプリを構築し、インストール済み PWA の自動セッション開始を設定する
- PWA を Vercel にデプロイし、有効でインストール可能な Web アプリマニフェストとアイコンを用意する
@meta-quest/bubblewrap-cliで公開済み PWA を署名済み Meta VR APK にパッケージ化する- TWA が起動するように署名キーストアと Digital Asset Links を設定する
ovr-platform-utilでビルドを Store にアップロードする- 2D アプリが読み込み中のままになる、没入型アプリに URL バーが表示される、TWA が起動しない、アップロードがブロックされるといった問題を解決する
IWSDK アプリ構築の詳細なガイダンスについては、hz-iwsdk-webxr スキルを参照してください。Store への提出プロセス全般(VRC 準拠、ストアアセット、審査状況の追跡)については、hz-store-submit スキルを参照してください。
パイプラインの概要
パイプライン全体は、次の順序で進みます。モード固有の差分には印を付けています。それ以外の手順は、2D と没入型で共通です。
0. Pick app mode → 2D panel vs immersive WebXR (sets steps 1 + 4)
1. Build the web app → IWSDK WebXR app (immersive) OR any responsive PWA (2D)
2. Deploy to Vercel → public HTTPS origin = <DOMAIN>
3. Manifest + icons → installable web app manifest, PNG icons, live on <DOMAIN>
4. Package as APK → bubblewrap: keystore, twa-manifest, build, asset links
5. Upload to the Store → ovr-platform-util upload-quest-build
各手順の依存関係は重要です。末尾の[手順の実行順序](#order-of-operations)を参照してください。
ステップ 0:最初にアプリモードを選ぶ
アプリモードは、一度だけ選択する最も重要な決定事項です。後続の変更点は、正確に 2 つです。
- Web アプリが起動時に WebXR セッションを自動的に開始するかどうか(没入型のみ)。
twa-manifest.jsonのhorizonOSAppModeの値("immersive"または"2D")。
| 2D PWA | 没入型 WebXR PWA | |
|---|---|---|
| 実行形態 | Horizon 上のウィンドウ型 2D パネル | 完全な VR/WebXR セッションを開始 |
| Web アプリ | 任意のレスポンシブ PWA(IWSDK は任意) | WebXR アプリ(手軽に実装するなら IWSDK) |
requestSession の自動開始 | いいえ — 追加しないでください(ステップ 1) | はい — アプリに組み込みます(ステップ 1) |
horizonOSAppMode | "2D"(ステップ 4) | "immersive"(ステップ 4) |
horizonOSAppMode の値を間違えるのは、よくある失敗の原因です。immersive に設定した 2D アプリは読み込み中のままになり、2D に設定した没入型アプリにはブラウザーの URL バーが表示されます。
詳しい選択ガイドについては、references/app-modes.mdを参照してください。
ステップ 1:Web アプリを構築する
没入型 WebXR アプリ(IWSDK)
唯一サポートされているスキャフォルダーである @iwsdk/create を使って、ひな形を作成します。
npx @iwsdk/create@latest <app-name> --yes --mode vr --no-metaspatial \
--no-physics --no-locomotion --grabbing
アプリに合わせて、--physics(Havok の重力/衝突判定)、--locomotion(広い空間を移動)、--grabbing(手やコントローラーでオブジェクトをつかむ)を切り替えてください。アーケードスタイルのアプリでは、物理演算よりも決定論的な手動モーションを優先してください。
IWSDK アプリコードを一から作り直さないでください。 テンプレートに含まれる CLAUDE.md、.claude/skills/iwsdk-* スキル、iwsdk-rag MCP が、インポート、ECS、XR 入力、物理演算、UI、デバッグに関する信頼できる情報源です。推測するのではなく、それらを参照してください。
没入型アプリの自動開始は、最初から組み込んでください。 インストール済みの没入型 PWA は 2D ページを表示せずに開くため、アプリ自体が読み込み時にセッションを開始する必要があります(アプリアイコンのタップがユーザーアクティベーションになります)。getDigitalGoodsService で条件を設定し、インストール済み PWA でのみ実行し、ブラウザータブでは実行されないようにしてください。
const nav = navigator as Navigator & { xr?: { isSessionSupported?: (m:string)=>Promise<boolean> } };
if ("getDigitalGoodsService" in window && nav.xr?.isSessionSupported) {
nav.xr.isSessionSupported("immersive-vr")
.then(s => { if (s) world.launchXR(); }) // IWSDK launchXR == requestSession + setup
.catch(() => {});
}
getDigitalGoodsService はデバイス上でのみ利用できます。ヘッドセットでこの動作経路を検証してください。
2D ウィンドウアプリ
レスポンシブな Web アプリ/PWA であれば動作します。IWSDK は不要です。独自のライブラリエントリーを持つ、単一インスタンスのスタンドアロンパネルとして実行されます。有効なインストール可能な PWA(ステップ 3)にして、一般的な静的サイト/SPA と同様にビルドしてデプロイしてください(ステップ 2)。上記の自動起動コードは追加しないでください。
すべてのスキャフォールディング用フラグ、プロジェクトの構成、および自動起動の理由については、references/app-modes.md を参照してください。
ステップ 2:Vercel にデプロイする
パッケージ化する前に、Web アプリを公開 HTTPS オリジンで公開しておく必要があります。bubblewrap がそこからマニフェストとアイコンを取得するためです。Vite の設定で base: "./" を設定してから、次を実行します。
npx -y vercel@latest whoami
npx -y vercel@latest teams ls
npx -y vercel@latest deploy --prod --yes --scope <team-slug>
次の 2 つの URL が生成されます。
- 正規のエイリアス
https://<project>.vercel.app→ 公開状態(200)。以降はすべての箇所でこれを<DOMAIN>として使用します。 - ハッシュ付きのデプロイごとの URL
…-<team>.vercel.app→ デプロイ保護により 401。共有には使用できず、<DOMAIN>としても使用できません。
ルートとマニフェストの両方が 200 を返し、マニフェストが application/manifest+json として配信されていることを確認します。
curl -s -o /dev/null -w "%{http_code}\n" https://<DOMAIN>/manifest.webmanifest
詳細と再デプロイと再ビルドの使い分けについては、references/vercel-deploy.md を参照してください。
ステップ 3:PWA のマニフェストとアイコン
どちらのモードでも、有効なインストール可能なマニフェストと PNG アイコンが必要です。bubblewrap update の実行前に、これらを <DOMAIN> で公開してください。public/manifest.webmanifest を配置します。
{ "name":"…","short_name":"…","description":"…","start_url":"/","scope":"/",
"display":"standalone","orientation":"landscape",
"background_color":"#06010f","theme_color":"#0a0418",
"icons":[
{"src":"/icons/icon-192.png","type":"image/png","sizes":"192x192","purpose":"any"},
{"src":"/icons/icon-512.png","type":"image/png","sizes":"512x512","purpose":"any"},
{"src":"/icons/icon-512-maskable.png","type":"image/png","sizes":"512x512","purpose":"maskable"}]}
index.html の <head> に、<link rel="manifest">、<meta name="theme-color">、<link rel="icon"> を追加してリンクします。複数のオリジンを使う 2D アプリでは、各オリジンに additional_trusted_origins とアセットのリンクを追加してください。
ここには ImageMagick/PIL がないため、SVG から sharp(npm i -D sharp)でアイコンを生成してください。マスカブルアイコンは全面を覆い、不透明である必要があります(透明部分や角丸は不可)。Vite は public/ を dist/ にコピーします。
アイコン生成スクリプトとマニフェストの完全なリファレンスについては、references/manifest-and-icons.md を参照してください。
ステップ 4:Meta VR APK としてパッケージ化する(bubblewrap)
bubblewrap は、公開中の PWA を署名済み Android APK(Trusted Web Activity)にパッケージ化します。
npm i -g @meta-quest/bubblewrap-cli # bin: bubblewrap
前提条件は ~/.bubblewrap に事前設定されています(専用の JDK 17 と Android SDK)。ツールの場所は動的に確認してください。
KT=$(find ~/.bubblewrap/jdk -path '*/bin/keytool' | head -1)
BT=$(ls -d ~/.bubblewrap/android_sdk/build-tools/* | sort -V | tail -1)
bubblewrap init は、実際の TTY が必要な対話型の inquirer ウィザードを使用します。Bash を操作するエージェントのように、TTY を使わない呼び出し元はウィザードに回答できず、これを回避する値指定フラグもありません。そのため、以下のスクリプトを使う方法を使用してください。
-
署名用キーストア — ビルド前に、使用するキーを開発者に確認してください。キーは永続的なものであり、今後の更新では毎回同じキーを再利用する必要があります。既存のキーストアを再利用する場合(公開済みアプリの更新時に必要です)は、そのパス、エイリアス、パスワードを確認してください。または、新しいキーストアをデプロイ可能な Web ツリーの外部に生成してください。
-
twa-manifest.json(スクリプトを使う方法)— 正式なTwaManifestスキーマに基づいて作成します。重要なフィールドは次のとおりです。"horizonOSAppMode": "immersive" // ← "immersive" for WebXR | "2D" for a 2D panel app
値を間違えるのは典型的な失敗例です(ステップ 0)。applicationId には Horizon App ID の数値を指定します("0" なら IAP なしでビルドおよびサイドロードできます。ストア関連の作業を始める前に実際の ID を設定してください)。
-
ビルド —
updateは Gradle を再生成し、バージョンを更新します。パスワードは環境変数で渡します(パスワード指定用の CLI フラグはありません)。cd <twa-dir> export BUBBLEWRAP_KEYSTORE_PASSWORD=<PW> BUBBLEWRAP_KEY_PASSWORD=<PW> bubblewrap update && bubblewrap build # → app-release-signed.apk + app-release-bundle.aab "$BT/apksigner" verify --print-certs app-release-signed.apk | grep -i SHA-256 # must == keystore -
Digital Asset Links — これに失敗すると、TWA は起動しません。パッケージ名とコロン区切りの 16 進数形式の証明書 SHA-256 を含む
public/.well-known/assetlinks.jsonを、同じドメインとすべての信頼済みオリジンでホストしてください。再デプロイした後、curl https://<DOMAIN>/.well-known/assetlinks.jsonを実行して確認します。
セキュリティ: キーストアとアプリシークレットを公開ホストに置くことは絶対に避けてください。curl -o /dev/null -w "%{http_code}" https://<DOMAIN>/android.keystore で確認します(404 が返ること)。キーストアのバックアップを取ってください。
キーストアの取り扱い、twa-manifest.json の完全なテンプレート、ビルドの検証、およびアセットリンクの詳細については、references/bubblewrap-packaging.md を参照してください。
Step 5: Meta Horizon Store にアップロードする
hzdb / metavr はデバイス専用のため、アップロードには使用できません。ovr-platform-util を使用します。同じコマンドで 2D ビルドと WebXR ビルドの両方に対応できます。アップロードする前に、実機のヘッドセットで署名済み APK を確認してください(metavr app install でインストールし、起動して、metavr log をざっと確認します)。Store の審査中に起動時のクラッシュが見つかると、対処に時間がかかります。
./ovr-platform-util upload-quest-build \
--app-id <HORIZON_APP_ID> --app-secret <SECRET> \
--apk app-release-signed.apk \
--channel ALPHA --age-group MIXED_AGES \
--notes "…" --disable-progress-bar
必須項目:--app-id、--apk、--channel、--age-group
(TEENS_AND_ADULTS | MIXED_AGES | CHILDREN)、および --app-secret または --token。
チャンネル:テストには ALPHA / BETA / RC、本番には STORE を使用します。認証にはアプリの App Secret(Dashboard → アプリ → API タブ)またはユーザートークンを使用します。ユーザーに確認してください。値を推測してはいけません。
初回に発生しやすいブロッカー: must first agree to our Developer Distribution Agreement — 組織の管理者が
https://developers.meta.com/horizon/manage/organizations/<ORG_ID>/legal-documents/ で一度署名する必要があります。いったん中断してユーザーに確認してから、同じコマンドを再実行してください。
ツールのダウンロード、認証、DDA ブロッカーについては、references/store-upload.md を参照してください。
実行順序
- 最初に 2D とイマーシブのどちらにするか決めます(Step 0)。これによって、自動起動の手順(Step 1、WebXR のみ)と
horizonOSAppModeの値(Step 4)が決まります。 bubblewrap updateを実行する前に、マニフェストとアイコンを公開しておく必要があります。これらは<DOMAIN>から取得されます。- TWA を起動する前に、すべての信頼済みオリジンで Asset Links を公開しておく必要があります。
- Web のみの修正なら、Vercel の再デプロイだけで済みます。インストール済みの TWA は次回の起動時に変更を反映します。APK を再ビルドして再アップロードする必要があるのは、ネイティブ側の変更(ID、名前、アイコン、バージョン、アプリモード、パッケージング)がある場合のみです。
注意点
horizonOSAppModeの不一致は最大の失敗要因です。"2D"にimmersiveを設定すると読み込み画面で停止し、"immersive"に2Dを設定すると URL バーが表示されます。値を修正して再ビルドしてください。- 署名キーは永久に使用するものです。更新のたびに同じキーストア、エイリアス、
packageIdを再利用する必要があります。キーストアを紛失すると、新しいアプリ登録が必要になります。バックアップを取り、デプロイ可能な Web ツリーの外に保管してください。 bubblewrap initには TTY が必要です。エージェントはスクリプト化された手順(手書きのtwa-manifest.json+update+build)を使用してください。- 自動起動はイマーシブ専用です。2D アプリには
launchXR()のコードスニペットを絶対に追加しないでください。また、ブラウザーのタブで実行されないよう、必ずgetDigitalGoodsServiceを条件にしてください。 getDigitalGoodsServiceはデバイス専用です。自動起動はデスクトップブラウザーやエミュレーターでは検証できません。ヘッドセットでテストしてください。- 正規の Vercel エイリアスを使用してください。デプロイごとに生成されるハッシュ付き URL はデプロイ保護によって 401 を返すため、
<DOMAIN>として使用できません。 - マスカブルアイコンは、全面に塗りつぶし、かつ不透明にする必要があります。透明部分や角丸があると、プラットフォームがマスクを適用した後にアーティファクトが目立ちます。
- Asset Links は起動の条件です。一致するパッケージ名と証明書の SHA-256 を含む
/.well-known/assetlinks.jsonが公開されるまで、TWA は「起動しません」。 - DDA が初回アップロードをブロックします。署名できるのは組織の管理者のみです。初回アップロードで最もよく発生する失敗です。
参考資料
- アプリモード:2D とイマーシブ — 選択ガイド、IWSDK のスキャフォールディング、プロジェクト構成、自動セッション起動の理由。
- Vercel へのデプロイ — Vite の設定、デプロイコマンド、正規 URL とハッシュ付き URL の違い、再デプロイと再ビルドの使い分け。
- PWA マニフェストとアイコン — マニフェストの詳細リファレンス、複数オリジンの設定、
sharpによるアイコン生成スクリプト。 - Bubblewrap によるパッケージング — キーストアの取り扱い、
twa-manifest.jsonのテンプレート、ビルドの検証、Asset Links、セキュリティ。 - Store へのアップロード —
ovr-platform-utilのダウンロード、認証、アップロードコマンド、Developer Distribution Agreement によるブロッカー。 - トラブルシューティング — パイプライン全体でよく発生する障害とその解決方法。