Communitygithub.com

divinevideo/divine-web

React Router SPA のサブドメインルート (/) で、URL リダイレクトなしに直接ページコンポーネントをレンダリングします。

divine-web とは?

このスキルは、ユーザー名ベースのサブドメイン(例:username.example.com)において、React Routerを使用したシングルページアプリケーション(SPA)で、ルートパス(/)にアクセスした際に、コンテンツを直接レンダリングするためのパターンを提供します。従来の手法では、サブドメインにアクセスすると /profile/ユーザーID などにリダイレクトされ、クリーンなサブドメインURLが失われたり、useParamsに依存するコンポーネントがルートパラメータ不足で空白ページを表示したりする問題がありました。このスキルは、エッジワーカー(Fastly Compute、Cloudflare Workersなど)がHTML内にユーザーデータを注入し、クライアント側のReactコンポーネントがルートパラメータがない場合にその注入データをフォールバックとして使用することで、リダイレクトなしにサブドメインルートでコンテンツを表示できるようにします。divinevideo/divine-webプロジェクトをはじめ、同様のサブドメインルーティングパターンを持つあらゆるReact SPAで利用可能です。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/divinevideo/divine-web/tree/HEAD/.agents/skills/spa-subdomain-root-render

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

SPA サブドメインルートでのリダイレクトなしレンダリング

問題

サブドメインベースのルーティング(例:username.example.com)を構築する際、React Router の <Navigate to="/profile/username" replace /> を使用すると URL が変更され、クリーンなサブドメイン URL を持つという目的が損なわれます。 さらに、ターゲットコンポーネントが useParams() に依存してルートパラメータを取得する場合、/ にはそのパラメータが存在しないため、空白ページが表示される可能性があります。

コンテキスト / トリガー条件

  • サブドメインプロファイルページが /profile/npub1... にリダイレクトされ、/ でコンテンツを表示しない
  • サブドメインルートでページが空白(コンテンツが読み込まれない)になる
  • コンポーネントが useParams() を使用しており、それらのパラメータがないルートでレンダリングされる
  • エッジワーカーが window.__SUBDOMAIN_USER__ または同様のグローバルにユーザーデータを注入する
  • username.domain.com/ を個人のウェブサイトのようにレンダリングしたい

解決策

1. ルーター: ナビゲーションではなくコンポーネントを直接レンダリング

変更前 (リダイレクト + URL 変更を引き起こす):

<Route path="/" element={
  subdomainUser
    ? <Navigate to={`/profile/${subdomainUser.npub}`} replace />
    : <Index />
} />

変更後 (URL 変更なしで / にレンダリング):

<Route path="/" element={
  subdomainUser
    ? <ProfilePage />
    : <Index />
} />

2. コンポーネント: フォールバックデータソースを追加

コンポーネントは、ルートパラメータが欠落している場合に、注入されたサブドメインデータをチェックする必要があります。

変更前 (ルートパラメータのみをチェック):

export function ProfilePage() {
  const { npub } = useParams<{ npub?: string }>();
  const identifier = npub;

  if (!identifier) {
    return <Error message="ユーザー識別子が指定されていません" />;
  }
  // ... コンポーネントの残りの部分
}

変更後 (注入データにフォールバック):

import { getSubdomainUser } from '@/hooks/useSubdomainUser';

export function ProfilePage() {
  const { npub } = useParams<{ npub?: string }>();
  const subdomainUser = getSubdomainUser();

  // ルートパラメータが優先され、サブドメインデータがフォールバック
  const identifier = npub || subdomainUser?.npub;

  if (!identifier) {
    return <Error message="ユーザー識別子が指定されていません" />;
  }
  // ... コンポーネントの残りの部分
}

3. エッジワーカー: HTML にユーザーデータを注入

エッジワーカー(Fastly Compute、Cloudflare Workers など)は、ユーザーデータを注入する必要があります:

// サブドメインを処理するエッジワーカー内
const userData = await fetchUserData(username);
const html = await fetchSpaHtml();

const injectedHtml = html.replace(
  '</head>',
  `<script>window.__DIVINE_USER__ = ${JSON.stringify(userData)};</script></head>`
);

return new Response(injectedHtml, { headers: { 'content-type': 'text/html' } });

4. クライアントサイドのデータアクセス

// useSubdomainUser.ts
interface SubdomainUser {
  subdomain: string;
  pubkey: string;
  npub: string;
  username?: string;
  displayName?: string;
  // ... その他のフィールド
}

export function getSubdomainUser(): SubdomainUser | null {
  if (typeof window === 'undefined') return null;
  return (window as any).__DIVINE_USER__ || null;
}

確認手順

  1. username.domain.com/ にアクセス - URL は / のまま、リダイレクトしないこと
  2. プロファイルコンテンツが正しく読み込まれること
  3. ブラウザコンソールで識別子欠落に関するエラーがないことを確認
  4. devtools で window.__DIVINE_USER__ に期待されるデータが含まれていることを確認

例

divine-web の場合:

  • thecomedianriley.divine.video/ は / で ProfilePage をレンダリング
  • ProfilePage は最初に useParams() をチェック(/ では空)
  • 注入されたデータから getSubdomainUser()?.npub にフォールバック
  • プロファイルが URL 変更なしで読み込まれる

注意点

  • このパターンにはエッジワーカーとクライアントコード間の連携が必要です
  • エッジワーカーは SPA のハイドレーション前にデータを注入する必要があります
  • 通常の /profile/:npub ルートが機能するように、常に最初にルートパラメータをチェックしてください
  • SEO を考慮: エッジワーカーは適切なメタタグも設定する必要があります
  • サブドメイン上のリンクは SmartLink コンポーネントを使用して、所有者のコンテンツにはローカルに留まり、 他のユーザーのコンテンツにはエイペックスドメインにリンクすることができます

参考資料

関連スキル