6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FlutterAdvent Calendar 2023

Day 16

Flutter Webでもシェア画像を動的に生成したい

Last updated at Posted at 2023-12-15

この記事は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画像を生成する形。

構成

構成としてはこんな感じ。

スクリーンショット 2023-12-13 10.06.29.png

慣れているのでunjs/nitroを使っているけど、
シンプルなので、なんでもいけるはず。

Flutter Webの資材

Web版のビルドを実行すると、

$ flutter build web --web-renderer canvaskit

こんな資材がbuild/web/ディレクトリ配下に生成される。

スクリーンショット 2023-12-13 7.42.23.png

index.htmlなどは、web/ディレクトリ配下にあるものがベースで、

スクリーンショット 2023-12-13 7.43.51.png

$FLUTTER_BASE_HREFなどの置き換えられている感じ。

スクリーンショット 2023-12-13 7.45.53.png

build/web/をそのまま、Hostingにデプロイしても動作するけど、
今回は特定のパスだけ、OGP画像を切り替えたいので、
index.htmlだけ、Cloud Functionsから返し、
他の資材は、Hostingから渡す形にしている。

全体の流れ

だいたいの流れとしてはこんな感じ。

スクリーンショット 2023-12-13 7.32.04.png

デフォルトのパスなど

特に書き換えが不要な場合は、なにもせず、
そのままの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.jsonserver/配下に配置

ビルド/デプロイの流れ

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.htmlpublic/から移動しておく必要がある。

サーバ側のビルド

サーバ側に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}}

このあたりは、過去に記事を書いたので、こちらもよかったら(*´ω`*)

おわりに

別途サーバが必要だけど、ちょっとしたもので、
シェア画像も対応できた(*´ω`*)

Flutter WebもSPAだと思っていろいろしてみると、
よりいいかんじにできそう(*´ω`*)


よかったら、ぜひ遊んでみてください〜(´ω`)

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?