Cloudflare Workersで複数アプリをサブディレクトリに同居させる方法

Cloudflare Workersをリバースプロキシとして使い、HonoやAstroで作られた複数のアプリを同一ドメインのサブディレクトリに配置する方法を解説します。

個人開発で複数のアプリやツールを作っていると、

「メインドメイン(example.com)のサブディレクトリ(/tools/calc/blog/)に複数のアプリを展開したい」

と思うことはないでしょうか。

今回は、Cloudflare Workers を「中央ルーター(リバースプロキシ)」として機能させ、複数のプロジェクトを同じドメイン下に共存させる方法を解説します。

構成の全体像

Cloudflare Workers でリクエストを受け取り、アクセスされたパスに応じて各バックエンド(Cloudflare Workers/Pages や Vercel など)へ振り分けます。

この「マイクロフロントエンド」構成には、以下のメリットがあります。

  • メインサイトの環境を一切変更せずに拡張できる
  • アプリごとに技術スタック(Astro、Hono、Next.js など)を自由に選択できる
  • 万が一ツールAが落ちても、メインサイトやツールBには影響しない
  • サーバーも自由に選べる

ここでは、以下のようなルーティングを構成します。

  • example.com/ → メインサイト
  • example.com/blog/ → ブログ(Astro製)
  • example.com/tools/ → ツール一覧ページ(Astro製)
  • example.com/tools/myapps/ → 自作アプリ(Hono製)
  • example.com/tools/calc/ → 計算機アプリ(Hono製)

ちなみに、私が運営している検索ポータルサイト fisea(ファイシー) も同じ構成になっています。

1. 中央ルーターとなる Worker の作成

まずはリクエストを振り分けるための Cloudflare Worker を作成します。

Cloudflare のダッシュボードから「Workers & Pages」を選択します。

Workers & Pages

「アプリケーションを作成する」をクリック。

select template

Hello World のテンプレートを選択します。

deploy

Workerの名前を設定して、一旦そのままデプロイ。

デプロイできたら、右上の「コードを編集する」をクリック。以下のコードを記述します。

サブディレクトリにホストする注意点として、ベースとなるパスをそのまま転送するか削ってから転送するか、フレームワークによって処理が異なります。

ここでは、 Astro(ベースパス設定あり・パスを削らない)と、Worker側でパスを削る処理が必要な Hono(ベースパス設定なし・パスを削る)という仕様が異なる2つのフレームワークを共存させるため、アプリごとに stripPrefix の設定を持たせています。

// worker.js
export default {
  async fetch(request) {
    const url = new URL(request.url);

    // スラッシュなしアクセスをスラッシュ付きにリダイレクト(301)
    // これにより相対パス(CSS や画像)の読み込みエラーを防ぐ
    if (url.pathname === '/tools' || url.pathname === '/blog') {
      return Response.redirect(`${url.origin}${url.pathname}/`, 301);
    }

    const pathSegments = url.pathname.split('/');
    const topDirectory = pathSegments[1]; // "tools" や "blog"

    // --- 転送先の設定辞書 ---
    // BLOG_STRIP_PREFIX, stripPrefix: true(パスを削る/Hono 等), false(パスを残す/Astro 等)
    
    // ブログ用の設定
    const BLOG_HOST = "my-blog.pages.dev";
    const BLOG_STRIP_PREFIX = false;

    const TOOLS_ROUTES = {
      "_root": { host: "my-tools-dashboard.pages.dev", stripPrefix: false },
      "myapps": { host: "myapps.xxx.workers.dev", stripPrefix: true },
      "calc": { host: "example-calc-app.vercel.app", stripPrefix: true },
    };

    let targetHost = "";
    let pathPrefix = "";
    let shouldStrip = true;

    // --- 分岐処理 ---
    if (topDirectory === 'blog') {
      targetHost = BLOG_HOST;
      pathPrefix = "/blog";
      shouldStrip = BLOG_STRIP_PREFIX;

    } else if (topDirectory === 'tools') {
      let appName = pathSegments[2];
      if (!appName || appName === "") appName = "_root";

      let routeConfig = TOOLS_ROUTES[appName];

      // 辞書にないパス(css, faviconなど)をルート(Astro)に流す
      if (!routeConfig) {
        appName = "_root";
        routeConfig = TOOLS_ROUTES["_root"];
      }

      targetHost = routeConfig.host;
      shouldStrip = routeConfig.stripPrefix;
      pathPrefix = appName === "_root" ? "/tools" : `/tools/${appName}`;

    } else {
      return new Response("Not Found", { status: 404 });
    }

    // --- 転送実行 ---
    if (shouldStrip && url.pathname.startsWith(pathPrefix)) {
      url.pathname = url.pathname.replace(pathPrefix, "");
      if (url.pathname === "") url.pathname = "/";
    }

    url.hostname = targetHost;

    // 新しいURLを基準に、元のリクエスト情報(POSTのボディや他の安全なヘッダー)を引き継ぐ
    const proxyRequest = new Request(url.toString(), request);
    
    // 転送先のサーバーが混乱しないよう、Hostヘッダーを転送先のドメインに強制的に書き換える
    proxyRequest.headers.set("Host", targetHost);

    // 再構築したリクエストで転送を実行
    return fetch(proxyRequest);
  },
};

2. Routes(ルート)の設定

この Worker を動かすうえで重要なのが、Cloudflare ダッシュボードでの Triggers(Routes)設定です。 ワイルドカード(/*)と完全一致(末尾スラッシュなし)の両方を登録するのがポイントです。

登録する Routes:

  • example.com/tools/*
  • example.com/tools(リダイレクト用)
  • example.com/blog/*
  • example.com/blog(リダイレクト用)

:::message example.com/tools(ワイルドカードなし)を登録し忘れると、スラッシュなしでアクセスした際に Worker が起動せず 404 になるため注意してください。 :::

3. 各アプリ側のベースパス設定

転送先のアプリ側でも「自分がサブディレクトリに配置されている」ことを認識させる必要があります。

Astro の場合

astro.config.mjsbase を指定するだけで、CSSなどの静的アセットのパスが自動的に解決されます。

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://example.com',
  base: '/tools', // サブディレクトリを指定
});

マークダウンで書かれたブログコンテンツの場合、画像リンクはベースパスを含めて記述する必要があります。

![example](/blog/images/blog/content/image.png)

Hono の場合

Hono では環境変数を使ってベースパスを切り替えます。ローカル開発時は BASE_PATH を空にし、本番では実際のパスを渡します。

すべてのリンクに BASE_PATH を付与する必要があります。

  1. wrangler.jsonc の設定
{
  "name": "my-hono-app",
  // ...
  "vars": {
    "BASE_PATH": "/tools/myapps"
  }
}
  1. Hono コードでの利用
// src/index.tsx
app.get('/', (c) => {
  const basePath = c.env.BASE_PATH || "";
  return c.html(`
    <html>
      <head>
        <link rel="stylesheet" href="${basePath}/style.css">
      </head>
      <body>...</body>
    </html>
  `);
});

ステップ4:SEO 対策(Canonical タグ)

元のデプロイ先 URL(*.workers.dev*.pages.dev)が公開されたままだと、Google に重複コンテンツとみなされるリスクがあります。各アプリの <head> に Canonical タグを設定して正規 URL を明示しましょう。

Astro での実装例:

---
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<link rel="canonical" href={canonicalURL} />

まとめ

Cloudflare Workers をルーターとして活用することで、メインドメインのSEO評価を維持しながら、複数のアプリを柔軟に追加・管理できる基盤が構築できます。

めちゃ便利なので複数のサイトのドメインを一つにまとめたい方や、個人開発でたくさんアプリを作っている方はぜひ試してみてください。

記事一覧に戻る