📘

Astro + Cloudlare PagesでSSGとSSRを併用するときの注意点

2023/06/30に公開

追記

@astrojs/cloudflare@6.7.0で大幅に改良されました!
簡単な構成では無駄のない記述になることを確認しました。
**/index.htmlの削除を始め、exclude主体で設定したところをinclude主体に変更したようです。
親はSSRで子がSSG(もしくはその逆)にした場合、どのような構成になるか確認していきたいですね。
https://github.com/withastro/astro/releases/tag/%40astrojs%2Fcloudflare%406.7.0

AstroとCloudflare最高!

いいところ

  • Astroのビルド爆速、動的で変更が多いところはSSRすればいいし便利
    • hybridモードでやれば、大体SSGで一部SSRとか簡単
    • Next.js(App Router)は静的なのか動的なのかよくわからなかった(実力不足)
  • Cloudflare Pages無料でほぼ制限なく運用できるのやばい

output: hybridでSSRしつつ、100件ほどSSGしたところローカルではビルドできるのに、Pages側のデプロイに失敗する事象が発生しました。デプロイのログを確認してみるとこんな一文が…

Error: Failed to publish your Function. Got error: Error 8000057: Rules in _routes.json are over the 100 rule limit. Refer to https://cfl.re/3FsE4aF.

どうやらWorkersを経由するか定義する_route.jsonのルールは100個までのようで、SSGでHTMLを生成した際に上限の100個を超えてしまったようです。dist/_route.jsonを確認したところ、静的に生成した全てのディレクトリとファイルが羅列されており、非常に無駄の多い記述となっていました。

補足

Pagesとは異なり無料プランではWorkersは通信量や通信数に制限があります。そのため、極力Workersを経由せずPagesの静的に生成したファイルにルーティングしたいです。そのための設定を記述するのが_route.jsonです。exclude: {...}に記述したパスはPagesに直接ルーティングされるようになります。

自動生成されたファイル
{
  "version": 1,
  "include": [
    "/*"
  ],
  "exclude": [
    "/404.html",
    "/_astro/404.9a2b609c.css",
    "/_astro/Counter.eb2f7b54.js",
    "/_astro/client.8a0a7111.js",
    "/_astro/hello.32e98718.css",
    "/_astro/index.ea0020fc.js",
    "/_worker.js",
    "/asset/manifest.json",
    "/counter/index.html",
    "/favicon/android-chrome-128x128.png",
    "/favicon/android-chrome-144x144.png",
    "/favicon/android-chrome-152x152.png",
    "/favicon/android-chrome-192x192.png",
    "/favicon/android-chrome-256x256.png",
    "/favicon/android-chrome-36x36.png",
    "/favicon/android-chrome-384x384.png",
    "/favicon/android-chrome-48x48.png",
    "/favicon/android-chrome-512x512.png",
    "/favicon/android-chrome-72x72.png",
    "/favicon/android-chrome-96x96.png",
    "/favicon/apple-touch-icon-114x114-precomposed.png",
    "/favicon/apple-touch-icon-114x114.png",
    "/favicon/apple-touch-icon-120x120-precomposed.png",
    "/favicon/apple-touch-icon-120x120.png",
    "/favicon/apple-touch-icon-144x144-precomposed.png",
    "/favicon/apple-touch-icon-144x144.png",
    "/favicon/apple-touch-icon-152x152-precomposed.png",
    "/favicon/apple-touch-icon-152x152.png",
    "/favicon/apple-touch-icon-180x180-precomposed.png",
    "/favicon/apple-touch-icon-180x180.png",
    "/favicon/apple-touch-icon-57x57-precomposed.png",
    "/favicon/apple-touch-icon-57x57.png",
    "/favicon/apple-touch-icon-60x60-precomposed.png",
    "/favicon/apple-touch-icon-60x60.png",
    "/favicon/apple-touch-icon-72x72-precomposed.png",
    "/favicon/apple-touch-icon-72x72.png",
    "/favicon/apple-touch-icon-76x76-precomposed.png",
    "/favicon/apple-touch-icon-76x76.png",
    "/favicon/apple-touch-icon-precomposed.png",
    "/favicon/apple-touch-icon.png",
    "/favicon/favicon.ico",
    "/favicon/icon-128x128.png",
    "/favicon/icon-144x144.png",
    "/favicon/icon-152x152.png",
    "/favicon/icon-160x160.png",
    "/favicon/icon-16x16.png",
    "/favicon/icon-192x192.png",
    "/favicon/icon-196x196.png",
    "/favicon/icon-24x24.png",
    "/favicon/icon-256x256.png",
    "/favicon/icon-32x32.png",
    "/favicon/icon-36x36.png",
    "/favicon/icon-384x384.png",
    "/favicon/icon-48x48.png",
    "/favicon/icon-512x512.png",
    "/favicon/icon-72x72.png",
    "/favicon/icon-96x96.png",
    "/favicon/site-tile-150x150.png",
    "/favicon/site-tile-310x150.png",
    "/favicon/site-tile-310x310.png",
    "/favicon/site-tile-70x70.png",
    "/hello/index.html",
    "/ssg/1/index.html",
    "/ssg/10/index.html",
    "/ssg/100/index.html",
    "/ssg/11/index.html",
    "/ssg/12/index.html",
    "/ssg/13/index.html",
    "/ssg/14/index.html",
    "/ssg/15/index.html",
    "/ssg/16/index.html",
    "/ssg/17/index.html",
    "/ssg/18/index.html",
    "/ssg/19/index.html",
    "/ssg/2/index.html",
    "/ssg/20/index.html",
    "/ssg/21/index.html",
    "/ssg/22/index.html",
    "/ssg/23/index.html",
    "/ssg/24/index.html",
    "/ssg/25/index.html",
    "/ssg/26/index.html",
    "/ssg/27/index.html",
    "/ssg/28/index.html",
    "/ssg/29/index.html",
    "/ssg/3/index.html",
    "/ssg/30/index.html",
    "/ssg/31/index.html",
    "/ssg/32/index.html",
    "/ssg/33/index.html",
    "/ssg/34/index.html",
    "/ssg/35/index.html",
    "/ssg/36/index.html",
    "/ssg/37/index.html",
    "/ssg/38/index.html",
    "/ssg/39/index.html",
    "/ssg/4/index.html",
    "/ssg/40/index.html",
    "/ssg/41/index.html",
    "/ssg/42/index.html",
    "/ssg/43/index.html",
    "/ssg/44/index.html",
    "/ssg/45/index.html",
    "/ssg/46/index.html",
    "/ssg/47/index.html",
    "/ssg/48/index.html",
    "/ssg/49/index.html",
    "/ssg/5/index.html",
    "/ssg/50/index.html",
    "/ssg/51/index.html",
    "/ssg/52/index.html",
    "/ssg/53/index.html",
    "/ssg/54/index.html",
    "/ssg/55/index.html",
    "/ssg/56/index.html",
    "/ssg/57/index.html",
    "/ssg/58/index.html",
    "/ssg/59/index.html",
    "/ssg/6/index.html",
    "/ssg/60/index.html",
    "/ssg/61/index.html",
    "/ssg/62/index.html",
    "/ssg/63/index.html",
    "/ssg/64/index.html",
    "/ssg/65/index.html",
    "/ssg/66/index.html",
    "/ssg/67/index.html",
    "/ssg/68/index.html",
    "/ssg/69/index.html",
    "/ssg/7/index.html",
    "/ssg/70/index.html",
    "/ssg/71/index.html",
    "/ssg/72/index.html",
    "/ssg/73/index.html",
    "/ssg/74/index.html",
    "/ssg/75/index.html",
    "/ssg/76/index.html",
    "/ssg/77/index.html",
    "/ssg/78/index.html",
    "/ssg/79/index.html",
    "/ssg/8/index.html",
    "/ssg/80/index.html",
    "/ssg/81/index.html",
    "/ssg/82/index.html",
    "/ssg/83/index.html",
    "/ssg/84/index.html",
    "/ssg/85/index.html",
    "/ssg/86/index.html",
    "/ssg/87/index.html",
    "/ssg/88/index.html",
    "/ssg/89/index.html",
    "/ssg/9/index.html",
    "/ssg/90/index.html",
    "/ssg/91/index.html",
    "/ssg/92/index.html",
    "/ssg/93/index.html",
    "/ssg/94/index.html",
    "/ssg/95/index.html",
    "/ssg/96/index.html",
    "/ssg/97/index.html",
    "/ssg/98/index.html",
    "/ssg/99/index.html",
    "/counter/",
    "/hello/",
    "/404/",
    "/ssg/1/",
    "/ssg/2/",
    "/ssg/3/",
    "/ssg/4/",
    "/ssg/5/",
    "/ssg/6/",
    "/ssg/7/",
    "/ssg/8/",
    "/ssg/9/",
    "/ssg/10/",
    "/ssg/11/",
    "/ssg/12/",
    "/ssg/13/",
    "/ssg/14/",
    "/ssg/15/",
    "/ssg/16/",
    "/ssg/17/",
    "/ssg/18/",
    "/ssg/19/",
    "/ssg/20/",
    "/ssg/21/",
    "/ssg/22/",
    "/ssg/23/",
    "/ssg/24/",
    "/ssg/25/",
    "/ssg/26/",
    "/ssg/27/",
    "/ssg/28/",
    "/ssg/29/",
    "/ssg/30/",
    "/ssg/31/",
    "/ssg/32/",
    "/ssg/33/",
    "/ssg/34/",
    "/ssg/35/",
    "/ssg/36/",
    "/ssg/37/",
    "/ssg/38/",
    "/ssg/39/",
    "/ssg/40/",
    "/ssg/41/",
    "/ssg/42/",
    "/ssg/43/",
    "/ssg/44/",
    "/ssg/45/",
    "/ssg/46/",
    "/ssg/47/",
    "/ssg/48/",
    "/ssg/49/",
    "/ssg/50/",
    "/ssg/51/",
    "/ssg/52/",
    "/ssg/53/",
    "/ssg/54/",
    "/ssg/55/",
    "/ssg/56/",
    "/ssg/57/",
    "/ssg/58/",
    "/ssg/59/",
    "/ssg/60/",
    "/ssg/61/",
    "/ssg/62/",
    "/ssg/63/",
    "/ssg/64/",
    "/ssg/65/",
    "/ssg/66/",
    "/ssg/67/",
    "/ssg/68/",
    "/ssg/69/",
    "/ssg/70/",
    "/ssg/71/",
    "/ssg/72/",
    "/ssg/73/",
    "/ssg/74/",
    "/ssg/75/",
    "/ssg/76/",
    "/ssg/77/",
    "/ssg/78/",
    "/ssg/79/",
    "/ssg/80/",
    "/ssg/81/",
    "/ssg/82/",
    "/ssg/83/",
    "/ssg/84/",
    "/ssg/85/",
    "/ssg/86/",
    "/ssg/87/",
    "/ssg/88/",
    "/ssg/89/",
    "/ssg/90/",
    "/ssg/91/",
    "/ssg/92/",
    "/ssg/93/",
    "/ssg/94/",
    "/ssg/95/",
    "/ssg/96/",
    "/ssg/97/",
    "/ssg/98/",
    "/ssg/99/",
    "/ssg/100/"
  ]
}

解決策

Astroはビルドする際にpublic/_route.jsonが存在すれば、dist/_route.jsonにコピーします。これを利用するため、手動でワイルドカードを用いた_route.jsonを定義しましょう。上記の非常に長いJSONから以下のようなスマートなJSONにすることでデプロイを通すことができました。

{
  "version": 1,
  "include": [
    "/*"
  ],
  "exclude": [
    "/_worker.js",
    "/404.html",
    "/404/*",
    "/favicon/*",
    "/asset/*",
    "/_astro/*",
    "/counter/*",
    "/hello/*",
    "/ssg/*"
  ]
}

_route.jsonを書く際の注意点

極力ワイルドカードで定義するために、以下の点に気をつけましょう。

  • public/src/pages/の直下にファイルを置かない
    public/src/pages/は区別なくルート直下にマッピングされるため、様々なファイルが配置され、ワイルドカードを使った定義が難しくなります(includeの設定を調整することで改善できる可能性あり)。なるべくディレクトリを切って、あるディレクトリ以下は全てSSGという設定にするとルールの数を減らすことができます。上記のJSONではpublic/以下にfaviconや静的ファイルのディレクトリ(public/favicon/,public/asset/)を作って、ルールの数を少なくしています。
  • 手動の設定なので実装に合わせてルールを追記する必要がある
    viteのプラグイン作成するなど、頑張れば自動化もできそうですが、基本は手動での追加になります。追加を忘れるとデプロイしても反映されない可能性があるので、レビューなどで注意して見る必要があります。

まとめ

思いもよらないところで、つまづきました… でもエラーメッセージでドキュメントが示されていて、丁寧なところが好感持てますね。他のCloudflare Pagesに対応しているフレームワークはどのような_route.jsonを生成するのか気になりますね。

備考

すでに議論されているようなので、近いうちに解決されるかもしれません。
https://github.com/withastro/astro/issues/6516
https://github.com/withastro/astro/issues/7188

Discussion