Cloudflare Pages Functionsで実現する、Next.jsサイトとSTUDIOサイトの共存方法
はじめに
こんにちは、株式会社TERASSのかんです。
現在、私たちのWebサイトはNext.jsで構築し、Cloudflare Pagesでホスティングしています。非常に快適な開発・運用環境なのですが、ある時こんな要件が生まれました。
「Webサイトは基本Next.jsで運用したいけど、特定のパスだけSTUDIOで作成した別サイトの内容を表示したい」
例えば、/ や /about はNext.jsのページ、でも /special-page は special.studio.site の内容を見せたい、といったケースです。
URLのドメインは統一したまま、裏側で異なるサービスをシームレスに切り替える...。今回はこの課題を Cloudflare Pages Functions を使って解決した方法をご紹介します。
実現したいこと
やりたいことを図にすると、このようになります。
ユーザーは同じ https://example.com というドメインにアクセスしているだけですが、パスによって表示されるコンテンツの出所が違う、という構成を目指します。これを実現するキーマンが、Cloudflare Pages Functionsです。
実現のキーマン: functions/[[path]].js
Cloudflare Pages Functionsは、Cloudflareのエッジで実行されるサーバーレス関数です。特定のパス、あるいはすべてのパスへのリクエストを横取りして、何らかの処理を挟むことができます。
プロジェクトのルートに functions ディレクトリを置き、その中に [[path]].js というファイルを作成すると、すべてのリクエストがこのファイルに書かれた処理を通過するようになります。まさにリクエストの門番ですね。
実際のコードと解説
それでは、今回実装した functions/[[path]].js のコードを見ていきましょう。
export async function onRequest(context) {
const { request, env } = context
const url = new URL(request.url)
const path = url.pathname
// このNext.jsプロジェクトで表示するパス(ページとアセットフォルダ)のリスト
// ご自身のプロジェクト構成に合わせてカスタマイズしてください。
const localPaths = [
"/",
"/_next", // Next.jsのシステムパス
"/images", // 静的アセット
"/fonts", // 静的アセット
"/about", // ページ
"/products", // ページ
"/blog", // ページ
"/robots.txt",
]
// リクエストされたパスが、上記リストのいずれかに合致するかチェック
const serveFromLocal = localPaths.some((p) => {
// `/` や `/products` のように、他のパスと前方一致してしまう可能性があるものは完全一致で判定
if (["/", "/products"].includes(p)) {
return path === p
}
// それ以外は前方一致で判定(例: /_next/static/... や /blog/post-1 などにマッチさせるため)
return path.startsWith(p)
})
if (serveFromLocal) {
// このサイトで表示すべきパスの場合、Cloudflare Pagesのアセットを返す
return env.ASSETS.fetch(request)
} else {
// それ以外のパスはSTUDIOサイトへプロキシする
const targetUrl = new URL(request.url)
const headers = new Headers(request.headers)
targetUrl.hostname = "YOUR_STUDIO_SITE_DOMAIN" // 転送先のホスト名 (ご自身のSTUDIOサイトのドメインに置き換えてください)
headers.append("x-studio-proxy-key", env.STUDIO_PROXY_KEY) // プロキシ経由を証明するヘッダー。Cloudflare Pagesの環境変数に STUDIO_PROXY_KEY を設定してください。
return fetch(targetUrl, {
method: request.method,
headers,
body: request.body,
})
}
}
このコードは、大きく分けて3つのことを行っています。
1. ローカルで処理するパスを定義
// このNext.jsプロジェクトで表示するパス(ページとアセットフォルダ)のリスト
// ご自身のプロジェクト構成に合わせてカスタマイズしてください。
const localPaths = [
"/",
"/_next", // Next.jsのシステムパス
"/images", // 静的アセット
"/fonts", // 静的アセット
"/about", // ページ
"/products", // ページ
"/blog", // ページ
"/robots.txt",
]
まず、Next.jsプロジェクトが責任を持つパスのリストを localPaths として定義します。ここには、ページ(/aboutなど)、Next.jsが必要とするシステムパス(/_next)、静的アセットを置くディレクトリ(/imagesなど)を含めます。このリストはご自身のプロジェクト構成に合わせて自由にカスタマイズしてください。
2. リクエストされたパスを判定
// リクエストされたパスが、上記リストのいずれかに合致するかチェック
const serveFromLocal = localPaths.some((p) => {
// `/` や `/products` のように、他のパスと前方一致してしまう可能性があるものは完全一致で判定
if (["/", "/products"].includes(p)) {
return path === p
}
// それ以外は前方一致で判定(例: /_next/static/... や /blog/post-1 などにマッチさせるため)
return path.startsWith(p)
})
次に、現在のリクエストのパス (path) が、先ほど定義した localPaths のいずれかに合致するかを判定します。
ここでのポイントは、前方一致 (startsWith) と 完全一致 (===) を使い分けている点です。
-
/blogのようなパスは、/blog/post-1や/blog/post-2といったサブパスにもマッチさせたいので 前方一致 を使います。 - 一方、
/productsを前方一致にすると、プロキシさせたい/products-specialのような無関係なパスまでマッチしてしまう可能性があります。これを防ぐため、特定のパスは 完全一致 で判定します。
3. パスに応じて処理を分岐
if (serveFromLocal) {
// このサイトで表示すべきパスの場合、Cloudflare Pagesのアセットを返す
return env.ASSETS.fetch(request)
} else {
// それ以外のパスはSTUDIOサイトへプロキシする
const targetUrl = new URL(request.url)
const headers = new Headers(request.headers)
targetUrl.hostname = "terass-inc.studio.site" // 転送先のホスト名
// ...
return fetch(targetUrl, { /* ... */ })
}
最後に、判定結果 serveFromLocal に応じて処理を分岐します。
-
trueの場合(Next.jsのパス):env.ASSETS.fetch(request)を実行します。これは「Cloudflare Pagesにデプロイされているビルド済みのアセットをそのまま返してください」というお決まりの命令です。 -
falseの場合(プロキシ対象のパス):fetchAPIを使って、リクエストを別のサーバーに転送します。-
targetUrl.hostnameをプロキシ先のドメイン(例:your-studio-site.studio.site)に書き換え。 -
STUDIO_PROXY_KEY環境変数から取得したカスタムヘッダーを追加。これにより、転送先のサーバーは「これはCloudflareからの正当なプロキシ経由のリクエストだ」と判断できます。
STUDIOではこのようなカスタムプロキシでのアクセスに対応しており、設定によって特定のヘッダーがないアクセスを弾く、といった制御が可能です。
詳細については、STUDIO 公式ブログ: カスタムプロキシを活用してSTUDIOサイトをサブディレクトリで公開する方法 をご参照ください。
-
これにより、クライアント(ブラウザ)とプロキシ先のサーバー(STUDIO)の間をCloudflare Pages Functionsが透過的に中継してくれる、リバースプロキシが完成しました。
まとめ
今回は、Cloudflare Pages Functionsを使って、Next.jsサイトと別ドメインのサイト(STUDIO)を単一のドメインで共存させる方法を紹介しました。
この方法のメリットは以下の通りです。
- ユーザー体験の向上: ユーザーはドメインを意識することなく、サイト内を回遊できる。
- 柔軟な技術選定: ページの特性に応じて、Next.jsとSTUDIOのような異なる技術スタックを使い分けることができる。
- 高速なエッジ処理: ルーティング処理がユーザーに近いCloudflareのエッジで行われるため、パフォーマンスの劣化を最小限に抑えられる。
functions/[[path]].js に数10行のコードを追加するだけで、これだけ強力なルーティングが実現できるのは、Cloudflare Pagesの大きな魅力の一つです。ぜひ皆さんも試してみてはいかがでしょうか。
Discussion