Flutter Web+microCMSでJamstackなサイトを作る


Flutter Web、いいですよね。2021年5月にはついに安定(していない)版も出てきてますます使いやすくなってきていると思います。

適当なブログ/ポートフォリオサイトを作りたい

たまにWebサイトを作りたい、と考えることがあると思います。Flutter Webであれば静的ホストできるので、Firebase HostingやGithub Pagesにアップすればすぐに作れるのですが、記事や作品など適宜更新したいコンテンツがあるといちいちコミット・デプロイしないといけないのが面倒というのがあります。またコンテンツが増えてくると管理が煩雑になる&再利用の観点からデータ部とソースコード部を分離して考えたいという欲求も出てきました。

そこでまずデータの分離に役立つのがheadless CMSです。

Headless CMSとは

まずCMSとは「Contents Management System」の略で、コンテンツをデータベースとして管理しておき必要に応じていい感じにWebサイトを生成してくれる便利なやつです。代表的なものとしてはWordPressが挙げられます。

その中でもheadless CMSというのはフロントエンド機能を持たず、コンテンツ取得等のAPIのみを提供するシステムになります。WordPressでは基本テンプレートからページを選択する必要がありましたが、headless CMSであればNext/Nuxt/PHP/Ruby/Mobileなど好みのフロントでシステムを構築することが可能になります。

有名なものではContentfulmicroCMSStrapiなどが挙げられます。

Jamstack

しかしheadless CMSを使うには一つ注意点があります。

headless CMSにはAPI経由でデータを取得するためページを動かすにはAPIキーが必要になります。ということは、ページの生成側でAPIキーを持たなければいけないのです。PHPなどサーバ側で生成するものであれば問題ないのですが、静的ホストされたサイトはどうしてもクライアント側で処理する必要があるため配信ファイル側にAPIキーが存在する状態になってしまいます。さすがにAPIキーを晒してしまうのは構造上よくありません。

この問題を解決するのがJamstackになります。

JamstackはWebサイト配信についてのアーキテクチャで、事前にHTML生成(pre-rendering)を行うことで速く安全でスケーラブルな配信を行うことができるというものです。

上図のように開発者がコンテンツを更新すると更新通知(webhook等)によりCIが回り、その時点でのデータとコードから静的ページをビルドしてデプロイします。サーバに乗った時点ですでにデータを持っているためAPIアクセスの必要がなく、先程のAPIキーの問題を解決できていることが分かります。

またコンテンツの更新ごとにCIを回しデプロイするため、リアルタイムは無理にせよ常にほぼ最新のコンテンツ状態にすることができます。

Jamstackを使ったフロント開発では軽量なGatsbyが有名だと思います。

しかし待てよ...

Flutter WebでもJamstackが使えるのでは!?

Flutter+microCMSでの構成例

ということでFlutter Webを使った構成例が以下になります。headless CMSにはmicroCMSを採用しています。これは一例なのでどのサービスでも構築可能だと思います。

構成は前の図を使うサービスに置き換えただけのものになります。FlutterでJamstackを使うにあたってはCIからCMSのデータを取得する仕組み部分が必要になるのですが、microCMSを使う場合はstatic_micro_cmsというパッケージを作ったのでこれを追加すれば使えます(型生成までできるよ!)。実際必要なのはコマンドでデータ全部取ってくるだけなので他のサービスで使いたい場合もさほど困らないと思います。

作ってみる

では簡単に作る流れを見ていきましょう。

microCMSの設定

まずmicroCMSのホームページからログイン/新規登録をし、サービスを作成します。

サービスができたら新たにコンテンツ(API)を作成し、適当にデータを用意します。

次いでに型生成に必要なのでAPIの[API設定]->[APIスキーマ]->[この設定をエクスポートする]からスキーマのjsonをダウンロードしておきましょう。

Flutter側の準備

続いてflutterプロジェクトの準備ですが、pubspec.yamlを以下のように書きます。

pubspec.yaml
#...
dev_dependencies:
  flutter_test:
    sdk: flutter
  static_micro_cms: 0.2.0

static_micro_cms:
  baseUrl: "https://[your-service-id].microcms.io/api/v1"
  apis:
    - endpoint: profile
      type: object
      schema: schema/api-profile-20211122080708.json
    - endpoint: news
      type: list
      schema: schema/api-news-20211121223418.json

baseUrlは自分のサービスのものにしてください。schemaは先程ダウンロードしたものなので、適当な場所においておきましょう(例では./schema/以下に入れてます)。

baseUrlはAPIプレビュー辺りから見られると思う

また、CIや手元でデータを取得したいときにはAPIキーが必要なので.envにキーを書いておきます

.env
API_KEY=[your api key]

ここまでできたらflutter pub get後にflutter pub run static_micro_cmsを呼ぶことで

  • types.microcms.g.dart
  • datastore.microcms.g.dart

の2つのファイルが自動生成されます。datastoreの方にデータがjsonで入っているわけですね。

またソースをGitHub等に上げる場合は適切にファイルを除外することが必要です。

.gitignore
*.microcms.g.dart
.env
# お好みで
# schema/

schemaは型生成に必要なので含めていますが、公開したくない場合にはCIを回す際に注入するのでも良いと思います。

CIとデプロイ

最後にfirebase hostingとGitHub Actionsを構築します。

一度GitHubに接続しておいて、上記事のようにfirebase init hosting:githubを実行することで自動的にGitHub Actionsのyamlを生成してくれます。
.envの生成とCMSからの情報の取得の処理を追加した一例は以下のようになります。

main.yaml
on:
  push:
    tags:
      - 'v*'

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
      - name: Setup Flutter
        uses: subosito/flutter-action@v1
      - name: Install
        run: flutter pub get
      - name: Create .env file
        env:
          MICRO_CMS_API_KEY: ${{ secrets.MICRO_CMS_API_KEY }}
        run: echo API_KEY=$MICRO_CMS_API_KEY > .env
      - name: Fetch api data
        run: flutter pub run static_micro_cms
      - name: Build
        run: flutter build web
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_FASTRIVER_DEV_MICRO }}'
          projectId: fastriver-dev-micro
          channelId: live

バージョンタグを切ると.envを生成、データを取得、ビルド、デプロイを行います。

microCMSからのwebhookの設定

microCMSはGitHub Actionsへのwebhookに対応しているのでその設定を行います。やり方は下の記事を参照してください。

トリガーイベントの名前は後で使うので適当で構いません。
次にwebhook検知時に動かしたいワークフロー(今回はmain.yaml)のon部分に以下を追記します。

main.yaml
on:
  repository_dispatch:
    types: [update_works]
#...

update_worksの部分は先に決めたトリガーイベントの名前です。配列なので多分複数指定可能になっています。

これでJamstackの環境が整いました! コードの更新/コンテンツの更新ごとにCIが回りサイトが更新されるようになっていると思います。

課題点

Flutter WebでmicroCMS・Jamstackを使う場合には今の所課題点がいくつかあります。

RichTextの表示

headless CMSは記事の本文などにRichTextというHTML形式のテキストを利用しています。これにより見出しや画像など記事の表現力が高まるのですがFlutter Webは現状(Webのくせに)HTMLの表現が苦手です。easy_web_viewwebviewxなどWeb対応のWebViewパッケージはいくつかあるのですが、スクロールを奪われたり縦横の大きさが固定だったりと実用としては難しい点があります。

Flutter Webのiframe周りの制約が大きいのが原因か

CIの時間

小規模なサイトですがCIを一度回すのに3分弱かかっています。Flutter SDKのセットアップに時間がかかっているようなのですがもう少し短くしたいです...

終わりに

課題はいくつかありますがheadless CMSのフロントにFlutter(Web)を利用する選択肢がとれる、というのは素晴らしいことと思います(CSSから逃げられるので)。Jamstackについてはかなり環境が整っているのでまだ伸びていく分野かなと思います。

ちなみにこの記事の通りに作ったサイトが以下のものになります。ありがとうございました。