🏙️

FlutterWebで動的OGPを実現する

2021/12/02に公開

FlutterWebでwebページを作った後に、そのままSNSやslackなどにリンクを貼ると、index.htmlに書いてあるOGP設定しか見ることができず、どのページをシェアしても同じタイトルと画像になってしまいます。

これをページごとに変えよう!というのが今記事のテーマです。

Flutter大学webアプリでは

Flutter大学には、webアプリがあり、質問zoomや勉強会zoomはこのwebアプリを経由するようにしています。(プランごとのアクセス権限を管理するため)

しかし、下記の2つの画像を見るとわかるように、/question_zoom/study_meetingのどちらも同じリンクプレビューが表示されてしまっています。

これは、index.htmlに書いてある、下記のOGP情報がどちらのページでも出てしまっているためです。

  <meta property="og:title" content="Flutter大学" />
  <meta property="og:description" content="アプリ開発が学べる日本最大級のFlutterコミュニティ" />

このままではぱっと見何のリンクか分かりにくくて不便なので、

質問zoomならタイトルを「質問zoom」、勉強会zoomならタイトルを「勉強会zoom」として、slackにシェアしたときに表示できるように実装することに挑戦したいと思います。

動的OGPを実現する手順

Reactで今回やりたいことを実現している記事を見つけました。

https://qiita.com/stin_dev/items/41ac4acb6ee7e1bc2d50

今回は、この記事と同様のことをFlutterWeb × Firebaseで実現したいと思います。

やることは以下です。

  • ①Firebase Functionsに動的にタイトルを取得してOGP設定を返すfunctionを作成する
  • ②Firebase Hosting設定を変える
    • 上で作ったfunctionにアクセスしてそのOGP情報を書き換える
    • firebase.jsonのrewritesにその設定書く
  • ③FlutterWeb側でリダイレクトしたurlに対応する

①Firebase Functionsに動的にタイトルを取得してOGP設定を返すfunctionを作成する

以下のように、ogp.tsというクラスにcreateOgpを作成します。

ogp.ts

import * as functions from "firebase-functions";

// 講師用OGP
export const createOgp = functions.https.onRequest(async (req, res) => {
    const path = req.path.split("/")[1];
    console.log(path);

    let title: string;
    let description: string;

    // switch文
    switch (path) {
        case "question_zoom":
            title = "質問zoom";
            description = "決まった時間にzoomを繋いで順番に技術的質問ができます。\n・Flutter修行プランの人は参加自由\n・アーカイブはアプリに上がります";
            break;
        case "spatial_cushion":
            title = "雑談SpatialChat";
            description = "誰でも参加自由\nSpatialChatというツールを使って交流しましょう!";
            break;
        case "study_meeting":
            title = "共同勉強会zoom";
            description = "誰でも参加自由\n・アーカイブはアプリに上がります";
            break;
        default:
            title = "Flutter大学webアプリ";
            description = "Flutter大学公式のwebアプリです。";
            break;
    }
    console.log('title is', title);

    try {
        res.set("Cache-Control", "public, max-age=600, s-maxage=600");
        const html = createHtml(path, title, description);
        res.status(200).send(html);
    } catch (error) {
        res.status(404).send("404 Not Found");
    }
});

以下のcreateHtmlという関数で、OGP設定のタグを作り、window.location="/${path}_"; にリダイレクトしています。pathの最後に_をつけることにより、もう一度同じurlが呼ばれてこのfunctionがループされることを防いでいます。

const createHtml = (path: string, title: string, description: string) => {
    return `<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Flutter大学</title>
    <meta property="og:title" content="${title}">
    <meta property="og:description" content="${description}">
    <meta property="og:type" content="article">
    <meta property="og:site_name" content="Flutter大学">
    <meta name="twitter:site" content="${title}">
    <meta name="twitter:card" content="summary">
    <meta name="twitter:title" content="${title}">
    <meta name="twitter:description" content="${description}">
  </head>
  <body>
    <script type="text/javascript">window.location="/${path}_";</script>
  </body>
</html>
`;
};

また、以下のようにindex.htmlでogp.tsを参照できるようにしときます。

index.ts

export { ogp };

②Firebase Hosting設定を変える

以下のように、FlutterWebのサイトのfirebase.jsonのrewritesの設定を追加します。例えば/question_zoomにアクセスしたときに、index.html ではなく、 ogp-createOgp を呼び出します。

{
  "hosting": {
    "target": "entrance",
    "public": "build/web",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "/question_zoom",
        "function": "ogp-createOgp"
      },
      {
        "source": "/spatial_cushion",
        "function": "ogp-createOgp"
      },
      {
        "source": "/study_meeting",
        "function": "ogp-createOgp"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

③FlutterWeb側でリダイレクトしたurlに対応する

①で無限ループしないように <script type="text/javascript">window.location="/${path}_";</script> と、pathの後ろに_をつけてリダイレクトしています。

https://pub.dev/packages/fluro というライブラリを使って、ルート管理をしているのですが、下記のようにして、_が後ろについたurlについても元の出したいページを返すようにしています。厳密に言えば、この後にurlを元に戻した方がいいのですが、、、今回はそのままにしています。

    static const String kUnderBarString = '_';
    FluroRouter router = FluroRouter();
    
    router.define(
      ZoomCushionPage.route + kUnderBarString,
      handler: Handler(
        handlerFunc: (context, params) {
          return ZoomCushionPage();
        },
      ),
      transitionType: TransitionType.none,
    );

詰まったところ

  • https://pub.dev/packages/url_strategy を使って、urlに#が出ないようにしないと、うまくfirebase.jsonのrewritesを反映できなかった
  • functionのregionをregion('asia-northeast1')にしたらうまくいかなかった

結果

以下のように表示できました!一旦サムネ画像はなしにしてますが、まあいいでしょうw

参考文献

https://qiita.com/stin_dev/items/41ac4acb6ee7e1bc2d50

https://firebase.google.com/docs/hosting/full-config?hl=ja

Flutter大学

Discussion