Flutter Web を静的HTML化して SEO に対応した話 — インデックス5ページが39ページになるまで
| 開発記録 | 公式サイト
タグ: #Flutter #Web #SEO #プリレンダリング #個人開発
Flutter Web で作った当サイトは、CanvasKit 描画のせいでクローラには「真っ白なページ」に見えていました。ビルド後プリレンダで当時の全42ルート(現在は46ルート)の静的HTMLを生成し、インデックス登録が5ページから39ページに増えるまでの実録です。
Flutter Web はそのままでは検索エンジンに「真っ白」に見える
この公式サイト(zumec.app)は Flutter Web で作っています。開発体験は最高なのですが、公開してしばらくして大きな問題に気づきました。Google Search Console でページを検査すると、クローラに見えているのはほぼ空の HTML だけ。Flutter Web の CanvasKit レンダラは画面全体を canvas に描画するため、本文のテキストが HTML 上に存在しないのです。
さらに致命的だったのは canonical タグです。SPA なのでどの URL を開いても同じ index.html が返り、全ページが「トップページの複製」として扱われていました。結果、公開から数ヶ月たってもインデックスされたのは 5 ページ前後。アプリ詳細もブログ記事も、Google から見れば「存在しないページ」でした。
選んだ対策: ビルド後プリレンダリング
SSR フレームワークへの移行も考えましたが、Flutter Web の資産を捨てたくありません。そこで「ビルド後に全ルートの静的 HTML を生成する」方式にしました。仕組みはシンプルです。
flutter build web --release --pwa-strategy=none
↓
dart run tool/prerender.dart --verify
↓ 全ルート分(導入時点で 42、現在は 46)の静的 HTML を build/web/ に生成
プリレンダスクリプトは、アプリが読むのと同じ JSON データ(アプリ一覧・ブログ記事)からルート一覧を組み立て、ルートごとに固有の title / canonical / OGP / JSON-LD / 本文 HTML を埋め込みます。ユーザーのブラウザでは従来どおり Flutter が起動して画面を置き換えるので、見た目は何も変わりません。クローラには本文が、ユーザーにはアプリが届く構成です。
ポイントは、プリレンダ本文と Flutter 側の表示内容を一致させること。中身が乖離するとクローキング(偽装)と判定されるリスクがあるためです。データソースを共通の JSON にしたことで、この整合は自然に保たれます。
.htaccess の whitelist と「ゴースト URL」対策
もう一つ地味に重要だったのが 404 の扱いです。SPA の定番設定「全パスを index.html に書き換える」だと、存在しない URL でも 200 が返ってしまい、ゴースト URL が量産されます。そこでプリレンダ時に既知ルートの whitelist を .htaccess に自動生成し、未知の URL には静的な 404.html を返すようにしました。
このとき一度事故も起きました。プリレンダを通さず flutter build web だけ実行してデプロイすると、whitelist が空のまま上書きされて全ルートが 404 になります。以来、ビルドは必ず検証つきのスクリプト経由と決めています。
結果: インデックス 5 ページ → 39 ページ
プリレンダ版をデプロイしたのが 5 月末。Search Console のインデックス登録済みページは、約 2 週間で 5 ページから 39 ページまで一気に増えました。当時の 42 ルート中 39 ページなので、ほぼ全ページが認識されたことになります。
Flutter Web で「コンテンツを検索に載せたい」サイトを作るなら、プリレンダはかなり現実的な選択肢です。フレームワークを乗り換えることなく、ビルドパイプラインに 1 ステップ足すだけで、クローラから見えるサイトに変わります。