add-malli-schemas
Efficiently add Malli schemas to API endpoints in the Metabase codebase with proper patterns, validation timing, and error handling
Add client-side routing (multiple pages) to an existing Metabase data-app project using the host-provided `DataAppRouter`, `DataAppLink`, and `useDataAppLocation` primitives. Use when the user has an existing data-app project and wants more than one page.
$ npx -y skills add metabase/metabase --skill metabase-data-app-routing --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/metabase-data-app-routingContext preview
The summary Claude sees to decide when to auto-load this skill.
Add client-side routing (multiple pages) to an existing Metabase data-app project using the host-provided `DataAppRouter`, `DataAppLink`, and `useDataAppLocation` primitives. Use when the user has an existing data-app project and wants more than one page.
name: metabase-data-app-routing description: Add client-side routing (multiple pages) to an existing Metabase data-app project using the host-provided `DataAppRouter`, `DataAppLink`, and `useDataAppLocation` primitives. Use when the user has an existing data-app project and wants more than one page.
A Metabase data-app bundle doesn't bundle a router library — it imports three small primitives from `@metabase/embedding-sdk-react/data-app`:
| API | Purpose | |---|---| | `<DataAppRouter>` | Wrap the app once. Tracks the current sub-path. Auto-detects the iframe's URL prefix; bundle author writes no basename. | | `<DataAppLink to="/customers/42">` | Internal navigation link. Renders a real `<a href>` so middle-click / cmd-click open in a new tab. | | `useDataAppLocation()` | Returns `{ pathname, navigate }`. Use `pathname` for match-by-equality / `startsWith` rendering; use `navigate(to)` for programmatic nav. |
That's the entire surface. **No `react-router` of any version, no `<BrowserRouter>`, no `<HashRouter>`.** The API is deliberately decoupled from any router library so a future Metabase version can swap the underlying implementation without touching bundle code.
The template already externalizes `@metabase/embedding-sdk-react/data-app` in `vite.config.ts`. You do NOT need to edit `vite.config.ts` to add routing — just edit `src/App.tsx` (and add more component files as needed) per the step below.
Import the routing primitives normally. `<DataAppRouter>` does NOT take a `basename` prop — it auto-detects the iframe's URL prefix (`/embed/apps/<name>`) in production and resolves to no prefix in the Vite dev preview.
`App.tsx` is pure content — no `<MetabaseProvider>` here. The SDK's dev preview entry (`DataAppDevProvider`) and the production host (`DataAppProvider`) each wrap the tree in their own realm.
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>
);
}Run `yarn dev`, click the links, watch the URL bar change. Reload at `http://localhost:5174/customers/42` and the dev preview lands on the customer route directly.
**If the app presents multiple tabs (or any top-level page switcher), the base path `/` MUST render the default — leftmost / first — tab's content, never a blank page, a "Not found", or an empty shell.** This is the single most common mistake: the app boots at `/`, no branch matches, and the user sees nothing until they click a tab. Don't rely on the user (or a later navigation) to select the first tab — the default tab is the initial state.
Two equivalent ways to guarantee it, depending on whether tabs are route-backed:
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
Verify by reloading the app at its base path (`/`) with a fresh load: the leftmost tab's content must be visible immediately, and that tab must read as selected in the tab bar.
You don't need to do anything for this. For context:
Metabase is the easy, open-source way for everyone in your company to ask questions and learn from data.
Repo: metabase/metabase
Efficiently add Malli schemas to API endpoints in the Metabase codebase with proper patterns, validation timing, and error handling
Add OpenTelemetry tracing spans to Clojure code following Metabase tracing conventions. Use when instrumenting backend code with trace coverage.
Add product analytics events to track user interactions in the Metabase frontend
Evaluate Clojure code via nREPL using clj-nrepl-eval. Use this when you need to test code, check if edited files compile, verify function behavior, or interact…
Review Clojure and ClojureScript code changes for compliance with Metabase coding standards, style violations, and code quality issues. Use when reviewing pull…
Guide Clojure and ClojureScript development using REPL-driven workflow, coding conventions, and best practices. Use when writing, developing, or refactoring…