docs/developer/dashboard/customization/quickstart.mdx
This guide assumes two things are running:
create-spree-app project it lives at apps/dashboard/; add it to an existing project with npx spree add dashboard, or scaffold it anywhere with npx spree add dashboard --template pointing at your own copy. Start it with pnpm dev.All customizations in this guide are edits to your dashboard app's own code. You don't need to scaffold a plugin — plugins are for distributing features to other stores.
Add an "Analytics" item to the dashboard sidebar.
Step 1. Open src/plugins.ts — the starter ships this file already wired up — and register a nav entry:
import { defineDashboardPlugin } from '@spree/dashboard-core'
import { BarChartIcon } from 'lucide-react'
defineDashboardPlugin({
nav: [{
key: 'analytics',
label: 'Analytics',
path: '/analytics',
icon: BarChartIcon,
// Built-in entries use positions 100–600 (Home … Reports).
// 650 places Analytics after Reports, at the end.
position: 650,
}],
})
Step 2. Save. The dev server hot-reloads and the sidebar now shows "Analytics".
Clicking it lands on a "Page not found" screen — we registered the nav entry but no page. Let's fix that.
Step 3. Create the page component at src/pages/analytics.tsx:
import { PageHeader } from '@spree/dashboard-core'
import { ResourceLayout } from '@spree/dashboard-ui'
export function AnalyticsPage() {
return (
<ResourceLayout
header={<PageHeader title="Analytics" subtitle="Store performance over time" />}
main={<p>Coming soon.</p>}
/>
)
}
Step 4. Register the route in src/plugins.ts:
import { defineDashboardPlugin } from '@spree/dashboard-core'
import { BarChartIcon } from 'lucide-react'
import { AnalyticsPage } from './pages/analytics'
defineDashboardPlugin({
nav: [{
key: 'analytics',
label: 'Analytics',
path: '/analytics',
icon: BarChartIcon,
position: 650,
}],
routes: [{
key: 'analytics',
path: '/analytics',
component: AnalyticsPage,
}],
})
Save. Click "Analytics" — your page renders inside the dashboard chrome, at /<store id>/analytics.
defineDashboardPlugin registers extensions against the dashboard's shared registries: nav, routes, slots, tables, settingsNav, formFields, and customFieldComponents. Your src/plugins.ts runs when the app boots (the starter's main.tsx imports it), so everything is registered before the first render. These are the exact same APIs distributable plugins use — the only difference is packaging.
The custom route mounts under /<store id>/analytics. Routes registered this way are matched at navigation time, so they can even be registered lazily after boot.
Building a distributable plugin rather than customizing your own app? Ship pages as file routes instead — they compile into the app's route tree, so links to them are type-checked. The registry form shown here is for in-app customization.