metabase-data-app-routing について
既存の Metabase Data App にルーティングを追加するためのガイドです。SDK のルーティング API を使い、ルーターライブラリに依存せず、デフォルトページやナビゲーション、パス判定を設定します。
主な機能
- 01SDK のルーティング API を使用
- 02パスの取得とナビゲーション
- 03中クリックや新規タブに対応するリンク
- 04iframe の URL プレフィックスを自動検出
- 05ベースパスのデフォルトページ設定
活用シーン
- 01URL パスに応じてページを表示
- 02Data App 内にナビゲーションを追加
- 03ボタンから指定パスへ移動
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ metabase/ metabase --skill metabase-data-app-routingインストール後、Claude Code で/metabase-data-app-routingを入力して読み込まれたか確認します。
詳細
データアプリにルーティングを追加する
Metabase のデータアプリバンドルにはルーターライブラリは同梱されていません。代わりに @metabase/embedding-sdk-react/data-app から 3 つの小さなプリミティブをインポートします。
| API | 目的 |
|---|---|
<DataAppRouter> | アプリを一度ラップします。現在のサブパスを追跡します。iframe の URL プレフィックスを自動検出するため、バンドルの作成者が basename を書く必要はありません。 |
<DataAppLink to="/customers/42"> | 内部ナビゲーションリンクです。実際の <a href> をレンダリングするため、ミドルクリック / cmd クリックで新しいタブで開けます。 |
useDataAppLocation() | { pathname, navigate } を返します。等値マッチ / startsWith によるレンダリングには pathname を使用し、プログラムによるナビゲーションには navigate(to) を使用します。 |
これがサーフェスのすべてです。どのバージョンの react-router も、<BrowserRouter> も、<HashRouter> も使いません。 この API はあえてどのルーターライブラリからも分離されているため、将来の Metabase バージョンがバンドルコードに触れることなく基盤の実装を差し替えられます。
このスキルを使う場面
- ユーザーが動作するデータアプリプロジェクトを持っている場合 — データアプリテンプレートからスキャフォールドされているため、ワンライナーの
vite.config.ts(dataAppConfig())と、ファクトリをエクスポートするsrc/index.tsxがすでに存在します。開発プレビューにはプロジェクト内のエントリがありません。SDK の開発プリセットがそれを配信します。 - ユーザーがバンドルで異なる URL(
/overview、/customers/:id)に異なるコンテンツをレンダリングしたい場合。 - このスキルを、プロジェクトをゼロからスキャフォールドするために使わないでください — これは既存のデータアプリプロジェクトにのみパッチを当てます。まだプロジェクトがない場合は、中断して、ルーティングを追加する前に新しいデータアプリのスキャフォールドから始めるようユーザーに伝えてください。
dataAppConfig() はすでに @metabase/embedding-sdk-react/data-app を外部化しています。ルーティングを追加するために vite.config.ts を編集する必要はありません — 以下の手順に従って src/App.tsx(および必要に応じて追加のコンポーネントファイル)を編集するだけです。
ステップ 1 — <DataAppRouter> で App.tsx をラップする
ルーティングプリミティブを通常どおりインポートします。<DataAppRouter> は basename プロパティを取りません — 本番環境では iframe の URL プレフィックス(/embed/apps/<name>)を自動検出し、Vite 開発プレビューではプレフィックスなしに解決します。
App.tsx は純粋なコンテンツです — ここに <MetabaseProvider> はありません。SDK の開発プレビューエントリ(DataAppDevProvider)と本番ホスト(DataAppProvider)が、それぞれ独自のレルムでツリーをラップします。
import { StaticQuestion } from "@metabase/embedding-sdk-react";
import {
DataAppRouter,
DataAppLink,
useDataAppLocation,
} from "@metabase/embedding-sdk-react/data-app";
function Nav() {
return (
<nav style={{ padding: 16, borderBottom: "1px solid #e5e7eb" }}>
<DataAppLink to="/" style={{ marginRight: 16 }}>Overview</DataAppLink>
<DataAppLink to="/customers/42">Customer 42</DataAppLink>
</nav>
);
}
function Page() {
const { pathname } = useDataAppLocation();
if (pathname === "/") {
return (
<div style={{ padding: 24 }}>
<h1>Overview</h1>
<StaticQuestion questionId={1} height={360} />
</div>
);
}
const customerMatch = pathname.match(/^\/customers\/(\d+)$/);
if (customerMatch) {
const id = Number(customerMatch[1]);
return (
<div style={{ padding: 24 }}>
<h1>Customer #{id}</h1>
<StaticQuestion questionId={id} height={360} />
</div>
);
}
return <div style={{ padding: 24 }}>Not found: {pathname}</div>;
}
export default function App() {
return (
<DataAppRouter>
<Nav />
<Page />
</DataAppRouter>
);
}
yarn dev を実行し、リンクをクリックして、URL バーが変わるのを確認します。http://localhost:5174/customers/42 でリロードすると、開発プレビューは直接 customer ルートに着地します。
読み込み時に常にデフォルト(左端)のタブを事前選択する
アプリが複数のタブ(または任意のトップレベルページスイッチャー)を提示する場合、ベースパス / は必ずデフォルト — 左端 / 最初 — のタブのコンテンツをレンダリングしなければならず、空白ページ、「Not found」、空のシェルを決して表示してはいけません。 これは最もよくある間違いです。アプリは / で起動し、どのブランチもマッチせず、ユーザーがタブをクリックするまで何も表示されません。最初のタブを選択することをユーザー(または後続のナビゲーション)に頼らないでください — デフォルトのタブが初期状態です。
タブがルートに基づくかどうかに応じて、それを保証する 2 つの同等の方法があります。
-
ルートに基づくタブ —
/に最初のタブをレンダリングする明示的なブランチを与え、マッチしない / ベースパスがそこに解決されるようにします。const TABS = [ { path: "/", label: "Overview", render: () => <Overview /> }, { path: "/customers", label: "Customers", render: () => <Customers /> }, { path: "/reports", label: "Reports", render: () => <Reports /> }, ]; function Page() { const { pathname } = useDataAppLocation(); // Exact-or-prefix match; fall back to the FIRST tab so `/` (and any // unknown sub-path) always shows the default tab, never a blank page. const active = TABS.find((t) => t.path !== "/" && pathname.startsWith(t.path)) ?? TABS[0]; return active.render(); } -
ローカル状態のタブ(ルーティングなし) — アクティブタブの状態を最初のタブに初期化し、最初のレンダリングでそれが表示されるようにします。
const [active, setActive] = useState(TABS[0].id); // default = leftmost tab
ベースパス(/)でアプリを新しくリロードして確認します。左端のタブのコンテンツがすぐに表示され、そのタブがタブバーで選択されていると読み取れる必要があります。
ナビゲーションが親 URL にどのように反映されるか
これについて何かをする必要はありません。参考までに:
- バンドルが
<DataAppLink to="/customers/42">を呼び出す → ホストのnavigateが/embed/apps/<name>/customers/42でpushStateを実行します。 - 親の
AppViewが iframe の URL 変更を監視し、親の URL バーに/apps/<name>/customers/42として反映します。/embedプレフィックスが取り除かれるのは、親の React Router のルートが/embed/...ではなく/apps/:name/*であるためです。 - リロードが機能するのは、BE がすべての
/embed/apps/:name/*サブパスに対して同じdata-app.htmlを配信するためです。iframe が起動し、<DataAppRouter>がwindow.location.pathnameを読み取り、プレフィックスを自動検出して、正しいサブパスから開始します。
やってはいけないこと
- プロジェクトに
react-router-dom(または任意のルータライブラリ)を追加しないでください。 バンドルには必要ありません。 - バンドル内で
<BrowserRouter>や<HashRouter>を使用しないでください。 これらは独自のsetStateフローを実行し、Near Membrane のバッチ処理バグに遭遇します。 - バンドル内から
window.history.pushStateを直接呼び出さないでください。<DataAppLink>またはuseDataAppLocationのnavigateを使用してください。 - 親の URL を読み書きしようとしないでください。 iframe の sandbox 属性が設計上それをブロックします。
一般的なパターン
パスに基づく条件付きレンダリング
const { pathname } = useDataAppLocation();
if (pathname === "/") return <Home />;
if (pathname.startsWith("/customers/")) return <Customer />;
return <NotFound />;
プログラムによるナビゲーション(例: <button> から)
function OpenCustomerButton({ id }: { id: number }) {
const { navigate } = useDataAppLocation();
return <button onClick={() => navigate(`/customers/${id}`)}>Open #{id}</button>;
}
パスパラメータを手動で解析する
組み込みの useParams はありません。自分でマッチングしてください:
const { pathname } = useDataAppLocation();
const match = pathname.match(/^\/customers\/(\d+)$/);
if (match) {
const id = Number(match[1]);
// ...
}
これを頻繁に行う場合は、バンドル内の独自のヘルパーに切り出してください:
function useCustomerIdFromPath(): number | null {
const { pathname } = useDataAppLocation();
const m = pathname.match(/^\/customers\/(\d+)$/);
return m ? Number(m[1]) : null;
}
トラブルシューティング
| 症状 | 修正 |
|---|---|
初回読み込み時に <DataAppLink> がプレーンテキスト(クリック可能なリンクではない)としてレンダリングされる。 | バンドルの読み込みが完了しておらず、フォールバックパスが表示されています。プロバイダのラップは自動的に提供されます(SDK 開発プレビューの DataAppDevProvider、またはホストの DataAppProvider)。アプリ内に <MetabaseProvider> を追加しないでください。バンドルが起動すると、フォールバックは実際のリンクに解決されます。 |
| URL は変わるが UI が変わらない。 | <BrowserRouter>/<HashRouter> がまだツリー内にあります。ルータライブラリを取り除き、代わりに <DataAppRouter> / <DataAppLink> を使用してください。Near Membrane と React 18 のバッチ処理の相互作用は、バンドル内で独自の setState を実行するすべてのルータを壊します。 |
<DataAppLink> を中クリックしても何も起こらない。 | リンクは event.button !== 0 を使用して左クリック以外をスキップします。イベントを飲み込む別のコンポーネントでラップしていないか確認してください。 |