この記事はFlutter Advent Calendar 2023の
シリーズ1の16日目の記事です。
Flutter on the WebでWebアプリが作れるようになったけど、
SPAなので、SEO部分が悩ましい。。
SEO系のパッケージはいくつかあるけど、
アプリを開いているときに切り替える形なので、
Twitter/Xなどでシェアした場合だとうまくいかない。。
ただ、SPAなので、昔にNuxtを使って、
head部分を置き換えるのが使えないかと試してみた(*´ω`*)
作ってみたアプリ
音声でしか投稿できないSNS
「VoitterX」
クソアプリ Advent Calendar 2023の記事で作ったアプリ。
(こっちでは音声認識パッケージのspeech_to_textについて書いてます)
チャットルーム的な部屋があり、その中に投稿していくアプリ。
各ルームのURLのときは、そのルーム名のOGP画像を生成する形。
構成
構成としてはこんな感じ。
- アプリ部分
- Firebase Hosting
- Flutter Web/CanvasKit
- サーバ部分
- データ部分
慣れているのでunjs/nitroを使っているけど、
シンプルなので、なんでもいけるはず。
Flutter Webの資材
Web版のビルドを実行すると、
$ flutter build web --web-renderer canvaskit
こんな資材がbuild/web/
ディレクトリ配下に生成される。
index.htmlなどは、web/
ディレクトリ配下にあるものがベースで、
$FLUTTER_BASE_HREF
などの置き換えられている感じ。
build/web/
をそのまま、Hostingにデプロイしても動作するけど、
今回は特定のパスだけ、OGP画像を切り替えたいので、
index.html
だけ、Cloud Functionsから返し、
他の資材は、Hostingから渡す形にしている。
全体の流れ
だいたいの流れとしてはこんな感じ。
デフォルトのパスなど
特に書き換えが不要な場合は、なにもせず、
そのままのindex.html
を返す
特定のパス
特定のパスの場合は、index.html
を書き換えて返す。
HTMLをパースするほどでもないので、
単純に文字列のreplaceする形。
今回、置き換えたのは、この4つ。
- タイトル(
<title>
) - シェア時のタイトル(
og:title
) - シェア時のURL(
og:url
) - シェア時の画像(
og:image
)
<head>
- <title>VoitterX</title>
+ <title>おためしの部屋 | VoitterX</title>
<meta name="description" content="音声でしか投稿できないSNS" />
<meta name="og:title" property="og:title"
- content="音声でしか投稿できないSNS | VoitterX"
+ content="おためしの部屋 | VoitterX"
/>
<meta name="og:description" property="og:description"
content="音声でしか投稿できないSNS"
/>
<meta name="og:url" property="og:url"
- content="https://voitterx.web.app"
+ content="https://voitterx.web.app/rooms/TY-itJa71A7emwoekki"
/>
<meta name="og:image" property="og:image"
+ content="https://voitterx.web.app/ogp.png"
- content="https://voitterx.web.app/__og-image__/image?roomId=TY-itJa71A7emwoekki"
/>
</head>
置き換えるデータは、パスパラメタからFirestoreのデータを取得してる。
OG画像生成用のパス
実際のシェア画像を生成もサーバでやるようにしているので、
/__og-image__/image?roomId=<roomId>
にアクセスすると、
PNG画像を返す処理もしている。
unjs/nitro+satori + sharpをつかったOG画像生成はこちら。
仕組みとしては、背景の画層を用意しておいて、
ルーム名を埋め込んで、PNG画像にしている形。
OG画像生成は自前でつくらなくてもCloudinaryとかを使う形でもOK
実際のディレクトリ構成
ディレクトリ構成はこんな感じ。
app/ ... Flutter部分
lib/
main.dart
web/
index.html
build/
web/
index.html
flutter.js
pubspec.yaml
server/ ... Server(nitro)部分
routes/
__og-image__/
image.ts ... OG画像生成
[...].ts ... index.htmlの書き換え
assets/
index.html
public/
flutter.js
.firebaserc
firebase.json
-
app/
とserver/
をそれぞれ用意 -
firebase.json
はserver/
配下に配置
ビルド/デプロイの流れ
Flutterをビルド
まずは、Flutter側をビルド。
app/build/web
を作成する。
$ cd app
$ flutter build web --web-renderer canvaskit
ビルドした資材をコピー
次に、ビルドした資材をサーバ側にコピーする。
$ cd ../server
# ビルドしたFlutter資材をサーバ側にコピー
$ cp -r ../app/build/web public
# 動的に返すものをassetsに移動
$ mv public/index.html assets/index.html
nitroのお作法では、以下のような感じ。
- public/ ... Hostingから配信
- それ以外 ... Functionsから配信
Firebase Hostingではパスにファイルが存在すると、
サーバ側を呼び出さなくなるため、
index.html
はpublic/
から移動しておく必要がある。
サーバ側のビルド
サーバ側にFlutterの資材を配置したら、
サーバ側をビルドする。nitroだとこんな感じ。
$ NITRO_PRESET=firebase nitropack build
ビルドが完了すると、server/.output
ディレクトリができあがる。
デプロイ
サーバ側もビルドしたら、Firebaseにデプロイする。
firebase.json
はこんな感じ。
// firebase.json
{
"functions": {
"source": ".output/server",
"runtime": "nodejs18"
},
"hosting": {
"public": ".output/public",
"cleanUrls": true,
"rewrites": [
{ "source": "**", "function": "server", "region": "asia-northeast1" }
],
"ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ]
},
}
画像やフォント、jsなどの資材は必要に応じて、キャッシュするように、
headers
を設定しておくといい感じ。
Firebaseの設定ファイルも用意できたら、
Firebase CLIを使ってデプロイする。
$ firebase deploy --only hosting,functions
GitHub Actionsでの自動デプロイ
Flutter用のGitHub Actionsがあるので、
それらを使えば自動デプロイもOK
name: deploy
"on":
push:
branches:
- develop
workflow_dispatch:
env:
APP_MODE: stag
PROJECT_ID: "YOUR_PROJECT_ID"
OIDC_SERVICE_ACCOUNT: "YOUR_OIDC_SERVICE_ACCOUNT"
OIDC_PROVIDER: "YOUR_OIDC_PROVIDER"
jobs:
build_and_deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
# *****************************************************
# * SETUP
# *****************************************************
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: "20"
cache: "pnpm"
cache-dependency-path: "server/pnpm-lock.yaml"
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.13.4"
cache: true
# *****************************************************
# * Build: Flutter
# *****************************************************
- name: "Build: Flutter"
run: |
cd app
flutter pub get
flutter build web --web-renderer canvaskit
cd ../
- name: "Build: Copy Public Assets"
run: |
cp -r app/build/web server/public
mv server/public/index.html server/assets/index.html
# Configure Workload Identity Federation via a credentials file.
- uses: google-github-actions/auth@v1
with:
service_account: ${{ env.OIDC_SERVICE_ACCOUNT }}
workload_identity_provider: ${{ env.OIDC_PROVIDER }}
- name: "Build Server And Deploy"
run: |
cd server
pnpm install
NITRO_PRESET=firebase nitropack build
pnpx firebase-tools deploy --only firestore:rules,functions,hosting --project ${{env.PROJECT_ID}}
このあたりは、過去に記事を書いたので、こちらもよかったら(*´ω`*)
- GitHub Actions+OIDCでCloud Runにデプロイする(Node.js) - くらげになりたい。
- GitHub Actions+OIDCでFirebase Hostingにデプロイする(Node.js) - くらげになりたい。
- GitHub ActionsでFlutterをビルドする(subosito/flutter-action) - くらげになりたい。
おわりに
別途サーバが必要だけど、ちょっとしたもので、
シェア画像も対応できた(*´ω`*)
Flutter WebもSPAだと思っていろいろしてみると、
よりいいかんじにできそう(*´ω`*)
よかったら、ぜひ遊んでみてください〜(´ω`)