LOST IN BLUE

2024/01/10

ブログ式年遷宮2024 with Cloudflare & Remix

Google Domain が売却されたので、ドメインを Cloudflare に移管しました。
ちょうどいい機会だったので、ブログシステムをRemix で作り、Cloudflare Pages + Functions + Cloudflare D1 + Cloudflare R2 で動かすようにしました。
Remix を選んだ理由としては、Cloudflare D1 を使ってみたかったからです。

Remix + Cloudflare でブログシステムを構築した際につまづいたポイントを書いておきます。
OGP を用いたリンクカードに関するものは、分量が多くなったので以下の記事で書きました。

Remix の Dynamic Segments で複数のパラメーターを受け取ることはできない

Remix の Dynamic Segments は、ファイル名を app/routes/article.$title.tsx のようにすると、https://example.com/article/hello-world のような URL でアクセスされた際に、hello-world の部分を title というパラメーターとして受け取ることができます。

今回作ったブログシステムでは、画像や音声はCloudflare R2 でホスティングするようにしています。
Remix では、Resource Routes というレンダリングするコンポーネントを持たない、ファイルの配信や動的に OGP 画像を作成して返すようなルートを作ることができます。

今回、Resource Routes を利用して、app/routes/assets.$key[.]$ext.ts とすることで、https://example.com/assets/ogp.png のような URL でアクセスされた際に、ファイル名と拡張子を別々に受け取ろうとしましたが、動きませんでした。
Remix の Dynamic Segments では、1 つの階層に対して 1 つのパラメーターしか受け取ることができないようです。

Remix を Cloudflare Pages で動かす際は、環境変数を process.env で取得できない

Remix では、以下のように loader 内で環境変数を取得して、いろいろやることができます。

export const loader = async () => {
  return json({ gaTrackingId: process.env.GA_TRACKING_ID });
};

しかし、Cloudflare Pages では上のコードは動きません。

そのため、Cloudflare Pages で動かす際は、以下のように context.env で環境変数を取得する必要があります。

export const loader = async ({ context }: LoaderFunctionArgs) => {
  const gaTrackingId = (context.env as Env).GA_TRACKING_ID ?? "";
  return json({ gaTrackingId });
};

D1 や R2 のバインディングと同じように、受け取る環境変数を Env の中に定義しておくのが良いと思います。

export interface Env {
  DB: D1Database;
  R2: R2Bucket;
  GA_TRACKING_ID: string;
}

Cloudflare Pages では、wrangler.toml に書いた設定は反映されない

D1 や R2 のバインディングを行う際に、wrangler.toml に以下のように設定を書きます。

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "hoge-database"
database_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

[[r2_buckets]]
binding = "R2"
bucket_name = "fuga-bucket"

以下のページの通り、Cloudflare は Wrangler を使っている場合は、ダッシュボードでワーカーの設定に変更を加えないようにして、wrangler.tomlを信頼できる唯一の情報源として扱うことを推奨しています。

wrangler deployをすると、wrangler.tomlの設定がワーカーに反映されます。
しかし、以下の issue の通り、Cloudflare Pages (Functions) を使っている場合のwrangler pages deployでは、wrangler.toml の設定は反映されません。

そのため、Cloudflare Pages (Functions) で Remix を動かす際は、プロジェクトの設定から D1 や R2 のバインディングを設定してあげる必要があります。
Settings > Functions > R2 bucket bindings と Settings > Functions > D1 database bindings で設定できます。
また、バインディングを設定しても、デプロイ済みのものには反映されないことを確認しています。
2024 年 1 月現在、バインディングの設定を変更した場合は、再度デプロイしないと反映されないようです。

この記事を書いている現在、Remix を Cloudflare で動かす例としてはCloudflare Workers を用いた例が多かったので、なんで D1 や R2 のバインディングができないのか分からずにつまづいてしまいました。
開発時はwrangler.tomlの設定を読んでくれることや、バインディングの設定が即反映されないのも混乱の原因でした。

感想

Remix はルーティング周りなどの思想が強めだったので、世界観が分かるまでちょっと大変でした。
Next.js はかなりマジックが多めでしたが、Remix は自分でちゃんとコードを書いたり設定しないといけない一方、細かいところまで自分でコントロールできるので、思想の違いを感じられて面白かったです。
静的ファイルを R2 から取ってくるエンドポイントを用意してサイト内で使う、といった Web 標準でできることをやろうとする分には、スムーズにできて非常に良かったです。

Cloudflare も D1 や Pages (Functions) など、今回のつまづきポイントからも開発途上な感じはあるものの、技術的に最新鋭のものがちゃんと使いやすく提供されており良かったです。

D1 や Pages・Remix のおかげかは分からないですが、以前の Next.js の SSG を使ったものと、今回の Remix + Pages (Functions) + D1 + R2 の記事の内容とブログシステム部分を分割したもので同等のパフォーマンスを出せていると思うので、満足です。

まだ Remix/Cloudflare のキャッシュ周りや、画像最適化処理などについては詰められていないので、今後も触っていきたいと思います。