こんな人向け: AIと一緒にFlutterでスマホアプリを作っていて、インストール不要のブラウザ版を自分のサイトに置きたい人
必要な前提: 手元でFlutterのアプリをビルドできること、ターミナルでコマンドを打てること、置き場所のサーバーにファイルと.htaccessを置けること(記事の例はXserverとWordPressのブログ)
読後にできること: ストア版を変えずにWeb版を作る流れ、つまずいた7つの点の直し方、同じドメインに置く前後の確認を、自分のアプリで使える
- 課題
- 解決できるようにすること
- 先に結論
- まずは一時URLで試して、それからブログのドメインへ
- 方針は「ストア版の動きは変えない」
- 手順の流れ
- つまずき1: 漢字が中国語のフォントで描かれた
- つまずき2: ブラウザでは、画面の向きを固定できない
- つまずき3: iPhoneのSafariだけ、効果音が鳴らなかった
- つまずき4: 絵とBGMが遅れて出る
- つまずき5: シェアのリンクが、一時URLから送ると404になった
- つまずき6: 更新したら、古いファイルと新しいプログラムが混ざって止まった
- ブログと同じドメインに置くときの約束ごと
- つまずき7: Xserverが、描画エンジン(.wasm)を圧縮してくれなかった
- 初回に読む量は、だいたいこのくらい
- 一般に案内する前に、計測とプライバシーポリシー
- ブラウザ版を作って置くときのチェックリスト
- こんな人に向いている
- まとめ
- 次に読むなら
課題
AIと一緒に、スマホのパズルアプリを個人で作っている。iPhone版はApp Storeに出していて、10月にも一筆書きパズルを1本公開した。
ただ、ストアのアプリは「インストールする」というひと手間がある。ちょっと気になった人に、その場で遊び始めてもらう入口があってもいい。
Flutter(Googleの、スマホ・Web・PCのアプリを1つのコードから作れる仕組み)で作っているので、理屈の上ではWeb版も作れる。じゃあ、ストア版を壊さずにブラウザ版を足して、しかもWordPressのブログが動いている同じドメインの下に置けるのか。今回の悩みどころは、そこだった。
解決できるようにすること
Flutterで作ったスマホアプリのブラウザ版を、ストア版の動きを変えずに作り、すでに動いている自分のサイト(ブログ)の下に、ブログを壊さずに置けるようにする。
持ち帰ってほしいのは、次の3つ。
- ストア版とWeb版を分けて作る手順の流れ
- 実際につまずいた7つの点と、それぞれの直し方
- ブログと同じドメインに置く前後に確かめることのチェックリスト
先に結論
10月26日に作り始めて、10月27日に、ブログと同じドメインの/games/の下でブラウザ版を公開した。
やったことを並べると、こうなる。
- ストア版の動きは変えない。Webだけの違いは、コードの中で「Webのときだけ」と分けた
- まずは7日で消える一時URLで試し、iPhoneの実機で確かめてから、ブログのドメインへ移した
- ビルド自体は1回で通った。つまずいたのは、ブラウザやサーバーとの相性のほう。日本語の漢字が中国語のフォントで描かれる、iPhoneのSafariで効果音だけ鳴らない、更新すると古いファイルと混ざって止まる、サーバーが描画エンジン(.wasm)を圧縮してくれない、など
- ブログ側には触れないように、
/games/の中の設定は最小限にして、置く前と後でブログの応答が同じかを確かめた - 一般に案内する前に、計測を入れて、プライバシーポリシーに書き足した
この記事でわかること
- ストア版を変えずに、Flutterアプリのブラウザ版を作る手順の流れ
- Flutter Webで日本語が中国語のフォントで描かれる原因と、外部のフォントを一切読まない直し方
- iPhoneのSafariで効果音が鳴らない原因と、Web Audioでの直し方
- WordPressのブログと同じドメインの下に、ブログを壊さずに置くときの約束ごと
- Xserverで.wasmが圧縮されないときに、圧縮済みのファイルで配るやり方
ここで、こう思う人もいるかもしれない。
「Flutterなら
flutter build webで終わりじゃないの?」
ビルドは、本当に1回で通った。手元のMacのChromeで開くと、ホームから盤面まで進めて、タップで遊べた。でも、そこからが長かった。通信の記録、iPhoneのSafari、自分のサーバーと、見る場所を変えるたびに別の問題が出てきた。この記事の中身は、ほぼ全部そっちの話になる。
まずは一時URLで試して、それからブログのドメインへ
最初からブログのドメインに置いたわけではない。
ブログは、広告の審査(Google AdSense)に向けて手を入れている最中だった。なので最初は、ブログに触れずに済む場所で試すことにした。使ったのは、アプリの紹介ページに使っているFirebase Hosting(Googleの、静的なページを置ける仕組み)の「プレビュー用チャンネル」。7日で消える一時URLが発行されるので、本番のサイトに触れずにスマホの実機で試せる。
iPhoneで試して直すところを直したあと、一般に公開する場所を決めた。自分が選んだのは、ブログと同じドメインのnoto-forest.com/games/。ブランドとブログとのつながりを考えて、同じドメインに寄せた。/games/は、将来ほかのゲームも並べる一覧の場所にした。
置き方は、ブログの運用を任せているAIのセッションに相談してから決めた。そこで決めた約束ごとは、後ろの節でまとめる。
方針は「ストア版の動きは変えない」
最初に決めたのは、これ。
- Webだけの違いは、Flutterの
kIsWeb(Webで動いているかどうか)で分ける - 画面の部品は、テストで「Webのとき」を再現できるよう、
isWebという引数で差し替えられるようにする - Web用のファイル(フォントなど)は
web/のフォルダに置く。ここはストア版のアプリには入らない - 画像や音を軽くするのは、ビルドした後の出力だけ。元の素材はストア版のまま
あと、Webで動かない部品は止めておく。アプリの利用状況の記録(Firebase)は、Web用の設定が無いまま起動すると失敗するので、最初はWebでは起動しない形にした(後で計測を入れるときに、Web用の設定を渡す形に変えた)。広告・レビュー依頼・クラッシュの記録は、共通の部品がWebでは起動しない作りになっている。
手順の流れ
ほかのアプリでもWeb版を作れるよう、最初の1本で分かったことを、自分が頼んでAIに手順書へまとめてもらった。その流れを短くすると、こうなる。
- アプリのフォルダで
flutter create --platforms=web --project-name <アプリのID> .を実行して、web/を作る。自動で作られるサンプルのテストは消し、書き換わった設定ファイル(.metadata)は元に戻す web/index.htmlとmanifest.jsonをアプリに合わせる。lang="ja"、題名と説明、テーマの色、アイコン(iOSの1024pxのアイコンから縮小して作る)- Webで動かない部品を止める(前の節)
- 日本語フォントを同梱する(つまずき1)
- スマホを横にしたときの案内を出す(つまずき2)
- ホームの下に、アプリ版(App Store)への入口を置く
- シェアをWeb用にする(つまずき5)
- ビルドして、手元で確かめる
- 一時URLに出して、スマホの実機で確かめる
- 本番の場所に置き、置く前と後でブログの応答を比べる
ビルドのコマンドは、こんな形になる。
flutter build web --release --no-web-resources-cdn --no-wasm-dry-run \
--base-href /games/<アプリの名前>/play/
--no-web-resources-cdnは、描画エンジン(CanvasKit)を外部のCDNから読まずに同梱する指定。--base-hrefには、置く場所のパスを入れる。
つまずき1: 漢字が中国語のフォントで描かれた
何もしないと、Flutter Webは、日本語の文字を描くための代わりのフォントとして、中国語(簡体字・香港)のフォントをGoogleのサーバー(fonts.gstatic.com)から取りに行き、漢字が中国語の字形で描かれた。AIがブラウザの通信の記録を見て、中国語のフォント(Noto Sans SC・HK)を取りに行っていることに気づいた。ブラウザの言語の設定には関係なく起きる(理由は下で書く)。
「日本語のゲームなのに、漢字の形が違う……」。自分は、中国語のフォントは使わないと決めた。
1回目の直し方では、止まりきらなかった
最初は、日本語フォントのNoto Sans JPを使う文字だけに絞って同梱し、テーマの文字にそのフォントを指定した。見た目は日本語らしくなった。ところが、通信を見ると、まだ一部の文字でGoogleのサーバーのフォントを取りに行っていた。
原因は「フォント名の無い文字の設定」
そこで、AIがFlutterのSDKの、描画エンジンが文字を扱う部分のコードを読んで原因を調べた。分かったのは、こういう仕組みだった。
- 文字の設定(TextStyle)にフォント名が無いと、エンジンは「調べるフォントが無い」ので、その文章の文字を全部「フォントに無い」とみなす。画面には既定のフォントで描かれるので、見た目では気づきにくい
- 1文字でも「無い」と判定されると、その文章の文字を全部、代わりのフォントの候補に入れる。だから、同梱したフォントに入っている漢字まで、中国語のフォントを取りに行く
フォント名が無かったのは、ツールチップなどが使う文字の設定と、ボタンの文字の設定だった。テーマの本文の設定(textTheme)だけ直しても足りなかったのは、これが理由。
直し方は、4つをそろえる
- 使う文字だけの日本語フォントを同梱する。アプリのコードと文章に出てくる、英数字以外の全部の文字(「…」「→」のような記号も)だけに絞った。このアプリでは1,444字で、普通と太字の2つ、それぞれ約572KB(圧縮して約300KB)。Noto Sans JPはSIL Open Font Licenseなので、ライセンスの文書(
OFL.txt)も同じフォルダに置く - 既定のフォント「Roboto」の中身を、同梱した日本語フォントにする。ビルドの後に、フォントの一覧(
FontManifest.json)の「Roboto」の行が同梱のフォントを指すように足した。これで、Robotoを外部へ取りに行かなくなる - テーマのすべての文字の設定に、フォント名を付ける。本文だけでなく、ツールチップやボタンの文字の設定まで。Webのときだけ
- 代わりのフォントを外部へ取りに行かない設定にする。
web/flutter_bootstrap.jsで、代わりのフォントの取得先を、ネットに出ないdata:のURLにした
_flutter.loader.load({
config: {
fontFallbackBaseUrl: 'data:text/plain,',
},
});
4の代わりに、同梱したフォントに無い文字は□になる。なので、画面に出す文言に絵文字は使わない。実際に、別のアプリの説明にあった「⚪︎」が日本語フォントに無かったので、「○」に置き換えた。文言を足したらフォントを作り直す、というのも手順に入れた。
確かめ方は、ブラウザの開発者ツールでperformance.getEntriesByType('resource')を見て、自分のサイト以外への通信が0件かどうか。ホーム、地図、盤面、遊び方、ほかのアプリの一覧まで回って、0件になったことを確かめた(このあと計測を入れたので、今は計測のプログラムの読み込みと計測の送信だけは外へ出る。フォントは外へ取りに行かない)。
つまずき2: ブラウザでは、画面の向きを固定できない
アプリ版は縦画面で固定している。でも、ブラウザでは向きを固定できなかった。
- 向きを固定する仕組み(Screen Orientation APIの
lock)は、AndroidのChromeで全画面表示にしたときしか効かない。iPhoneのSafariや、SNSアプリの中で開くブラウザには無い manifest.jsonの向きの指定は、ホーム画面に追加して開いたときだけ効く
なので、固定するのはあきらめて、「スマホを横にしたら案内を出す」形にした。Webで、横長で、しかも短い辺が600未満(=スマホ)のときだけ、「端末を縦にして遊んでください」と出す。PCやタブレットの横長の画面では、今までどおり縦長の帯を真ん中に置いて遊べる。PC専用の画面は作らないことにした(AIの提案を受けて、自分で決めた)。
つまずき3: iPhoneのSafariだけ、効果音が鳴らなかった
一時URLを自分のiPhoneのSafariで開いて遊んでみると、盤面をなぞる操作はアプリと変わらなかった。ところが、効果音が鳴らない。BGMは鳴っているし、消音のスイッチもオフ。
「BGMは鳴るのに、効果音だけ?」。手元のPCのChromeでは気づけない問題で、ここは実機で触って初めて分かったところ。
AIが調べた原因は、こうだった。
- 効果音に使っていた部品(
audioplayersのWeb版)は、音を鳴らすたびに新しいAudioContext(ブラウザで音を鳴らす土台)を作る - iPhoneのSafariでは、タップと同時に作られなかった
AudioContextは、止まった状態で始まる。作れる数にも上限がある - BGMは、iPhoneでは別の部品(
just_audio)で鳴らしていたので鳴った - PCのChromeは、一度画面を操作すれば鳴らせるので、手元では問題が出なかった
直し方は、AudioContextを1つにまとめること。最初のタップ(pointerdown・touchend・keydown)でその1つを開き、効果音はfetchで読んでdecodeAudioDataで変換した音を、その中で鳴らす。一度開けば、タップと同時でなくても鳴らせる。同じ音は2回目から読み込まない。Webのときだけこちらを使い、ストア版は今までどおり。直した版で、自分のiPhoneでも鳴ることを確かめた。
この直しは、複数のアプリで使っている共通の音の部品に入れたので、ほかのアプリのWeb版でもそのまま効く。
ちなみに、ブラウザは利用者が画面を触るまで音を鳴らさない。起動時にBGMを流す作りでも、エラーにはならず鳴らないだけなので、「最初のタップの後に鳴るか」を実機で確かめる項目も手順に入れた。
つまずき4: 絵とBGMが遅れて出る
iPhoneで遊んでいて、もう2つ気になった。地図や遊び方、クリアの画面で絵が遅れて出る。BGMが鳴り始めるまでに時間がかかる。
Web版は、画像や音を表示するとき(鳴らすとき)に取りに行く。しかも地図の絵はPNGで1枚約2.3MB、BGMは1曲約1〜2MBあった。
直し方は、ビルドの後に出力のフォルダだけを軽くするスクリプトをかけること。
- 画像: 透明な部分の無い大きなPNGを、ファイル名はそのままでJPEGの中身に置き換える。Flutter Webは画像の種類をファイル名ではなく中身の先頭で見分けるので、コードを変えずにそのまま読める。画像の合計は12.1MBから1.3MBに、地図1枚は2.3MBから約260KBになった
- 先読み: ホームが出たあと、その先の画面の絵を裏で読んでおく(
precacheImage) - BGM: iPhone用のm4aはAAC 96kbps、それ以外用のoggはOpus 64kbpsで作り直す。m4aは約2MBから約1MBになった
1つ注意がある。このスクリプトは1回だけかける。2回かけると、BGMをもう一度変換して音質が落ちる。
つまずき5: シェアのリンクが、一時URLから送ると404になった
iPhoneで試したときに、もう1つ見つかったのがシェア。
まず、Flutterでよく使う共有の部品(share_plus)のWeb版は、ブラウザに共有メニュー(Web Share API)が無いと、メールの画面(mailto:)を開いてしまう。PCのブラウザやSNSアプリの中のブラウザでは起こりうる。そこでWebでは、下から選択肢を出す形にした。共有メニューはあるときだけ出し、ほかに「Xに投稿」「LINEで送る」「文章をコピー」を選べる。
そして、やらかしたのがシェアの文章に入れるURL。最初は本番のURLを固定で書いていた。すると、一時URLで試しているときに送ったリンクが404になった。まだ本番には置いていなかったから。
今は、開いているページの場所(FlutterのUri.base)をそのまま入れている。これなら一時URLでも本番でも、後でサーバーを移しても正しいリンクになる。シェアの文章には、紹介ページではなくWeb版のURLを入れた。受け取った人が1回のタップで遊べるように。
つまずき6: 更新したら、古いファイルと新しいプログラムが混ざって止まった
Flutter Webのビルドの出力は、ファイル名にハッシュ(中身が変わると変わる文字列)が付かない。main.dart.jsも、文言を入れたJSONも、毎回同じ名前になる。
なので、ブラウザやサーバーが古いファイルを覚えていると、新しいプログラムが古いJSONを読んで止まる。実際に止まったのは手元の1回で、もう1回は起きる前に気づいて直した。
- 手元の確認で、Pythonの簡易サーバーで配っていたら、ブラウザが古いJSONを使い続けて「ui_text.jsonに節がありません」で止まった。この簡易サーバーはキャッシュの指定を付けないので、ブラウザが勝手に覚えてしまう。
Cmd+Shift+Rでも、Flutterがfetchで読むファイルは更新されないことがあった - 最初の一時URLには「1時間は覚えてよい」(
max-age=3600)を付けて出していた。このままだと更新のたびに同じことが起きかねないので、no-cacheにして、新しいチャンネル名で出し直した
no-cacheは「毎回、変わっていないかをサーバーに確かめる」という指定で、変わっていなければ中身は送られない。毎回ダウンロードし直すわけではないので、ハッシュの付かないFlutter Webとは相性がいい。手元で確かめるときは、開発者ツールの「キャッシュを無効化」を使うのが確実だった。
ブログと同じドメインに置くときの約束ごと
ここからが、置き場所の話。ブログはXserverの上で、WordPressが/blog/の下で動いている。ゲームを置いた時点(2026年10月27日)では、ドメインの/に来た人を/blog/へ転送していた(翌28日に、/はNoto-Forestのホームに切り替えた。ルートの転送の1行を外しただけで、ブログと/games/の置き方は変えていない)。

ブログ側のAIのセッションに相談して決めた約束ごとは、次の4つ。
/games/のフォルダを置いても、ブログの転送には影響しない。ルートの設定が「実在するファイルやフォルダは、そのまま返す」形になっているため/games/の中の設定ファイル(.htaccess)にRewriteEngine Onを書かない。書くと、その下ではルートの書き換えの規則が効かなくなる。中の設定は、ブラウザとサーバー側のキャッシュの指定と、ファイルの種類の指定だけにした- FlutterのURLは「#/」付き(ハッシュ)のままにする。
/games/の下でも、実在しないパスはブログへ回されて、WordPressの404になる。Flutterで「#」の無いURL(usePathUrlStrategy)にすると、画面ごとのURLが実在しないパスになってしまう。ハッシュのままなら、サーバーに届くのはいつも実在するindex.htmlの場所になる - 置く前と後で、ブログの応答が同じかを確かめる。
/、/blog/、/robots.txt、/sitemap.xml、/ads.txtをcurl -sIで見て、状態コードと転送先を比べる
4は、置いたあとに毎回やっている。この記事の下書きのときにも、AIが同じ確認をした(2026年10月27日)。
| URL | 応答 |
|---|---|
/ | 301 → /blog/ |
/blog/ | 200 |
/robots.txt | 200 |
/sitemap.xml | 301 → /blog/wp-sitemap.xml |
/ads.txt | 301 → /blog/ads.txt |
/games/、説明のページ、ゲーム本体 | 200(Cache-Control: no-cache) |
/games/の下の実在しないパス | 301 → /blog/games/…(WordPressへ) |
もう1つ、ページの分け方も相談ですすめられて、そうした。ブログは、広告の審査で「有用性の低いコンテンツ」とされたことがある。文章の少ないページだけを増やさないよう、/games/<アプリ>/は遊び方・ルール・開発の記録へのリンクを書いた説明のページにして、ゲーム本体はその先の/play/に置いた。
あと、置き方の小さなコツ。ゲーム本体はビルドの結果なので、リポジトリには入れていない。説明のページなどの正本だけをリポジトリに置き、配置のたびに「ページ+その場でビルドしたゲーム本体」を組み立てて、SSHでまとめて送る。送るのはAIで、配置のたびに自分が承認している。
つまずき7: Xserverが、描画エンジン(.wasm)を圧縮してくれなかった
本番に置いて確かめると、描画エンジンのcanvaskit.wasmが約5.6MBのまま届いていた。JavaScriptは圧縮されて届く(この記事の下書きのときの確認でもbrだった)のに、.wasmは圧縮されない。/games/の.htaccessに圧縮の指定(mod_deflate)を書いても効かなかった。
そこで、サーバーに圧縮してもらうのはやめて、圧縮済みのファイルを置く形にした。ビルドの後に.wasmをgzipで圧縮した.wasm.gzだけを置き、その場所の.htaccessに次を書く。
Options +MultiViews
RemoveType .gz
AddEncoding gzip .gz
AddType application/wasm .wasm
こうすると、canvaskit.wasmを求められたときに、圧縮済みの中身をContent-Encoding: gzip付きで返せる。いきなり本番に当てず、試験用のフォルダで確かめてから適用した(試験用のフォルダは消した)。描画エンジンは5.6MBから2.1MBになった。
この記事の下書きのときにAIが確かめても、Chrome向けのcanvaskit.wasmはcontent-encoding: gzipで約2.18MB、main.dart.jsはbr(brotli)で約1.0MBで届いていた。
ちなみに、試しに使ったFirebase Hostingは自動で圧縮してくれるので、この手間は要らなかった。置く場所を変えたら、何が圧縮されて届いているかを見直す、というのが今回の学び。
初回に読む量は、だいたいこのくらい
最初の一時URL(Firebase Hosting、brotli圧縮)で測った、初回の読み込みの内訳。画像とBGMを軽くする前の数字になる。
| 中身 | 大きさ |
|---|---|
| 描画エンジン(Chrome向け) | 1.6MB |
main.dart.js | 0.74MB |
| 日本語フォント(普通・太字) | 約0.6MB |
| ホームの背景 | 1.9MB |
| ホームのBGM | 1.2MB |
| 初回の合計 | 約6MB |
- ビルドの出力(
build/web)は全部で約61MBになる。描画エンジンのいくつもの版が入っているためで、1回の訪問で読むのは上の分だけ - Safariは描画エンジンが大きい版を読むので、もう少し増える(ファイルの大きさから出した見込みで、gzipで約7MB)
- Xserverに置いた直後(
.wasmを圧縮済みで配るようにした後、中国語のフォントを直す前)に、MacのChromeで測ったときは約8.4MBだった。上の約6MBとの差の内訳は測っていない。違いとして考えられるのは、ホームが出た後に先の画面の絵(約2MB)を裏で読む処理が入っていたこと、描画エンジンがbrotliの1.6MBではなくgzipの約2.1MBだったこと、機能を足してmain.dart.jsが約1.0MBに増えていたこと、まだ外部のフォントを取りに行っていたこと。画像とBGMは軽くした後だった
一般に案内する前に、計測とプライバシーポリシー
置いた時点では、ブログやSNSからの案内はまだしていない。案内するのは、計測を入れてから、と決めていた。何人が開いて、何人が遊び始めて、どこまで進んだかを、アプリ版と同じ物差しで見たいから。
やったのは、次の4つ。
- Web版にもFirebase Analyticsを入れる。Web用のアプリを登録して、Webのときだけ、その設定を渡して起動する形にした。設定が無ければ何もしない作りなので、ストア版は変わらない。本番のURLで、計測の送信が出ていることを確かめた
- ブログとは別の計測の場所(GA4のプロパティ)にする。同じにすると、ブログの数字にゲームが混ざる。ブログからゲームへのリンクには、どこから来たかが分かる印(UTM)を付ける
- プライバシーポリシーに書き足す。ウェブサイトとブラウザ版のゲームで、Googleのサービスに何が送られるか(Cookie等の識別子、閲覧したページ、ゲームの中の操作、ブラウザの種類など)、送り先、目的、送信を止める方法(Googleのオプトアウト アドオン)を書いた。ゲームの進み具合はブラウザの中だけに保存されて、サーバーには送らないことも書いた
- Web用のAPIキーを、自分のドメインからしか使えないように絞る。Google Cloudの管理画面で設定した。Web版のキーはページの中に入るので、ほかのサイトで使い回されるのを防ぐため。ただ、これで計測の記録そのものへのいたずらまでは防げない
1つ注意がある。外部への送信には、電気通信事業法の「外部送信規律」(利用者の情報を外部に送ることを知らせる決まり)がある。チャット機能の無いブラウザゲームが対象になるかは、総務省のFAQを見てもはっきりしなかった。なので、対象であっても足りるよう、送り先・情報・目的を書いておいた。これは法的な判断ではなく、念のための対応になる。
ブラウザ版を作って置くときのチェックリスト
今回やったことを、ほかのアプリでも使える形にまとめておく。
作るとき
- Webだけの違いは
kIsWebで分け、ストア版の動きを変えない - Webで動かない部品(分析・広告・レビュー依頼・クラッシュの記録)を止める
- 日本語フォントを使う文字だけに絞って同梱し、
OFL.txtも置く - 既定のフォント「Roboto」の中身を同梱のフォントにし、テーマのすべての文字の設定(ツールチップ・ボタンも)にフォント名を付ける
- 代わりのフォントを外部へ取りに行かない設定にする。画面の文言に絵文字を使わない
- スマホを横にしたときの案内を出す(向きは固定できない)
- シェアのURLは固定で書かず、開いているページの場所を入れる
- 効果音は1つの
AudioContextで鳴らし、最初のタップで開く - ビルドの後に、画像とBGMを軽くする(1回だけ)
確かめるとき
- 手元では開発者ツールの「キャッシュを無効化」を使う
- フォントを外部へ取りに行っていないか(
performance.getEntriesByType('resource')で、自分のサイト以外への通信を見る) - 外部の中国語のフォントを読んでいないか(通信を見る)
- iPhoneのSafariの実機で、最初のタップの後にBGMと効果音が鳴るか
- 再読み込みしても、進み具合が残るか
置くとき
- まず一時URLで試してから、本番の場所へ
- 置き場所の配信に
Cache-Control: no-cacheを付ける - 本番で、
.wasmが圧縮されて届いているか(content-encoding)を見る - ブログと同じドメインなら、子のフォルダの
.htaccessにRewriteEngine Onを書かない - FlutterのURLはハッシュ(
#/)のままにする - 置く前と後で、
/・/blog/・/robots.txt・/sitemap.xml・/ads.txtの応答を比べる - 文章の少ないページだけを増やさない(説明のページ+その先にゲーム)
案内する前
- 計測を入れ、ブログとは別の場所で数える
- プライバシーポリシーに、送る情報・送り先・目的・止め方を書き足す
こんな人に向いている
AIと一緒にFlutterでスマホアプリを作っていて、「インストールなしで遊べる入口も作りたい」と思っている人に向いている。特に、WordPressのブログなど、すでに動いているサイトのドメインの下に置きたい人には、置く前後の確認と.htaccessの約束ごとがそのまま使えると思う。
日本語のアプリをFlutter Webにして、「なんだか漢字の形が違う」「iPhoneだけ音が出ない」で止まっている人にも、原因の当たりを付ける材料になるはず。
まとめ
Flutterのアプリは、ビルドだけならすぐにブラウザ版になった。手間がかかったのは、その先。気づかないうちに取りに行く中国語のフォント、iPhoneのSafariでは無音の効果音、一時URLでは404になるシェアのリンク、ハッシュの付かないファイル名ではキャッシュの食い違い、Xserverでは圧縮されない.wasm。どれも、見る場所を変えて初めて出てきた。ストア版の動きは変えず、ブログにも触れずに置けたのは、Webだけの違いを分けたことと、置く前後で同じ確認をしたことが大きかった。
| 項目 | 内容 |
|---|---|
| 結論 | ストア版の動きは変えずに、Webだけの違いを分けてブラウザ版を作り、ブログと同じドメインの/games/の下に、ブログの応答を変えずに置けた。つまずきは7つで、どれもブラウザやサーバーとの相性だった |
| 教訓 | 手元のPCのChromeで動いても終わりにしない。通信の記録、iPhoneのSafari、本番のサーバーで、それぞれ確かめる |
| 対象読者 | AIと一緒にFlutterでスマホアプリを作っていて、ブラウザ版を自分のサイトに置きたい人 |
同じ状況に陥ったときの回避策
- 漢字の形がおかしいときは、外部のフォントへの通信を見る。フォント名の無い文字の設定が残っていないかを探す
- iPhoneで効果音だけ鳴らないときは、鳴らすたびに
AudioContextを作っていないかを見る - 更新後に起動しなくなったら、まずキャッシュを疑う。Flutter Webの配信は
no-cacheにする - 置き場所を変えたら、
.wasmが圧縮されて届いているかを見直す - 既存のサイトの下に置くときは、子のフォルダで書き換えの規則を持たず、置く前後で既存のURLの応答を比べる
次に読むなら
ブラウザ版にしたこのパズルを、どう企画して、App Storeに出す前に何を確かめたかは、別の記事にまとめている。入口を増やす前に、遊びの中身の違いをどう作ったかも合わせて読むと、全体の流れがつかみやすい。
- 混み合った一筆書きパズルで、前提を1つ反転して新作を作った話。App Store提出前に数字で確かめた6つの条件 — ジャンルの当たり前を1つだけ反転して遊びの核を決め、試作の数字で弱点を直し、提出の直前に競合の調べ直しと全問の集計で違いを確かめた手順とチェックリストが分かる
ブラウザ版そのものは、説明のページから遊べる。
この記事は、筆者が実際に個人開発しているモバイルアプリの開発記録(2026年10月26日〜27日)を基に、独自調整したAIが下書きを作成し、筆者が事実確認・編集を行った上で公開しています。

