Skip to main content
Both the admin panel and vendor portal use the same SDK (@mercurjs/dashboard-sdk). Customization is file-based and convention-driven — you add pages by creating files, configure navigation through exports, and override components through config.
Mercur does not use Medusa’s admin customization. If you’re coming from Medusa, forget defineWidgetConfig, defineRouteConfig, and widget injection zones — none of them exist here. Mercur ships its own admin and vendor panels, built on a shared extension framework with three mechanisms: drop-in routes (file-based pages that can also replace built-in pages), compound-component re-composition (every built-in page exposes its parts as slots), and named layout overrides (sidebar, topbar, store setup). The trade: instead of injecting widgets into fixed zones around a page Medusa controls, you compose the actual page — which is what makes a two-panel marketplace customizable without forking.

Choosing your extension mechanism

Junior-friendly rule of thumb: adding something new? Drop in a route. Changing something that exists? Re-compose it. Changing the frame around every page? Override a component. Sharing it across projects? Make it a block.
You want to…UseWhy it’s the right tool
Add a new screen or featureA drop-in page.tsx routeNew URL, auto-registered, sidebar entry via the config export — see Add a page
Change part of an existing pageA drop-in route at the same path, re-composing the page’s compound slotsYour route replaces the built-in one; untouched slots keep all their behavior — see the re-compose tutorial
Change global chrome (sidebar, topbar, store setup)A component override in the plugin configOne component, applied everywhere its slot renders — see Replace components
Reuse a feature across projectsA blockShips API + admin + vendor files installable with mercurjs add — see the build-a-block tutorial
The middle row is the one Medusa developers miss most. Because built-in pages are exported as compound components (e.g. ProductListPage from @mercurjs/vendor/pages with Header, HeaderActions, DataTable slots), “customize the products page” doesn’t mean forking it — it means rendering it with your own composition and reusing the slots you don’t change.

Set up

All configuration lives in the panel app’s Vite config — there is no separate config file.
1

Register the plugin

Add mercurDashboardPlugin to the panel app’s vite.config.ts. The only required option is medusaConfigPath — the plugin reads panel paths and ports from your API’s Medusa config:
vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { mercurDashboardPlugin } from '@mercurjs/dashboard-sdk'

export default defineConfig({
  plugins: [
    react(),
    mercurDashboardPlugin({
      medusaConfigPath: '../../packages/api/medusa-config.ts',
      name: 'My Marketplace',
      logo: 'https://example.com/logo.svg',
    }),
  ],
})
Projects created with create-mercur-app ship with this already wired for both panels.
2

Pass environment values explicitly

The plugin doesn’t read .env itself — load environment variables in vite.config.ts (e.g. with Vite’s loadEnv) and pass them into the plugin options, as the starter template does with backendUrl.
3

Restart after config changes

Options are applied at build time through virtual modules. Adding or removing routes hot-reloads automatically, but changes to the plugin options require a dev-server restart.

Configuration options

OptionTypeDescription
medusaConfigPathstringRequired. Path to your API’s medusa-config.ts, relative to the panel project root
backendUrlstringMedusa backend URL (default: http://localhost:9000)
vendorUrlstringAbsolute vendor portal URL including its path prefix
namestringApplication name shown in the sidebar
logostringURL to a logo image
componentsobjectComponent overrides (see Replace components)
i18nobjectInternationalization settings ({ defaultLanguage })
enableSellerRegistrationbooleanEnable the public seller registration flow (vendor portal)
imageLimitnumberMax upload size for images in bytes (default: 2 MB)

Add a page

1

Create the route file

Create a page.tsx inside src/routes/ and export a default React component. The route is determined by the file path:
src/routes/reviews/page.tsx
import { Star } from "@medusajs/icons"
import type { RouteConfig } from "@mercurjs/dashboard-sdk"

export const config: RouteConfig = {
  label: "Reviews",
  icon: Star,
  rank: 10,
}

export default function ReviewsPage() {
  return <div>Reviews</div>
}
2

Add the config export for navigation

A sidebar item is generated only when the page exports a config with a label. Pages without one are still routed — they just don’t appear in the menu.
PropertyTypeDescription
labelstringRequired. Text shown in the sidebar menu
iconComponentTypeIcon component (e.g. from @medusajs/icons)
ranknumberSort order — lower numbers appear first
nestedstringParent path for nested menu items
translationNsstringi18n namespace for the label
publicbooleanIf true, the route is accessible without authentication
Route files may also export a loader (React Router data loader) and handle (route metadata) alongside the default component.
3

Open it in the running panel

Start the dev server — the SDK picks the file up automatically. This example creates a /reviews route with a “Reviews” sidebar item. No route registration, no configuration file.
Matching paths replace, new paths append. If your route’s path matches a built-in page (e.g. src/routes/products/page.tsx/products), your page replaces the built-in one — that’s the override mechanism, no registration needed. Any other path is added alongside the built-in routes. Delete the file and the built-in page returns.

Routing conventions

File paths map to URL routes automatically:
File pathRouteDescription
src/routes/page.tsx/Root page
src/routes/reviews/page.tsx/reviewsStatic segment
src/routes/reviews/[id]/page.tsx/reviews/:idDynamic segment
src/routes/reviews/[[id]]/page.tsx/reviews/:id?Optional dynamic segment
src/routes/search/[*]/page.tsx/search/*Catch-all
src/routes/(settings)/page.tsxRoute groupingGroups routes without adding a URL segment
src/routes/dashboard/@sidebar/page.tsxParallel routeRenders alongside parent
Sidebar items are generated from pages that export a config with a label.

Ordering

Use rank to control the order. Lower values appear higher:
// src/routes/orders/page.tsx
export const config: RouteConfig = {
  label: "Orders",
  icon: ShoppingCart,
  rank: 1,
}

// src/routes/products/page.tsx
export const config: RouteConfig = {
  label: "Products",
  icon: Tag,
  rank: 2,
}

Nested menus

Use nested to group pages under a parent:
src/routes/settings/shipping/page.tsx
export const config: RouteConfig = {
  label: "Shipping",
  nested: "/settings",
}
This places “Shipping” as a child item under /settings in the sidebar.

Branding

Set name and logo in the plugin options to customize the sidebar header:
vite.config.ts
mercurDashboardPlugin({
  medusaConfigPath: '../../packages/api/medusa-config.ts',
  name: 'WeTest',
  logo: 'https://ui-avatars.com/api/?name=WeTest&background=18181B&color=fff&size=200&bold=true&format=svg',
})

Replace components

1

Write the replacement

Create the component anywhere under src/. It must have a default export:
src/components/custom-topbar-actions.tsx
export default function CustomTopbarActions() {
  return (
    <div>
      <button>Help</button>
      <button>Notifications</button>
    </div>
  )
}
2

Point the config at it

Register it in the components option. Paths are relative to src/:
vite.config.ts
mercurDashboardPlugin({
  medusaConfigPath: '../../packages/api/medusa-config.ts',
  name: 'My Marketplace',
  components: {
    MainSidebar: 'components/custom-sidebar',
    SettingsSidebar: 'components/custom-settings-sidebar',
    TopbarActions: 'components/custom-topbar-actions',
  },
})
3

Restart and verify

Restart the dev server — your component now renders everywhere its slot appears. Remove the entry and restart to get the built-in back; the originals are never modified.
ComponentDescription
MainSidebarPrimary navigation sidebar
SettingsSidebarSettings section sidebar
TopbarActionsAction buttons in the top bar
StoreSetupStore setup screen for new sellers (vendor portal)
These four slots are the complete list — there is no generic “replace any component” registry. If what you want to change isn’t one of them, it’s a page concern: re-compose the page instead.

Internationalization

1

Create translation resources

src/i18n/index.ts
export default {
  en: {
    reviews: {
      title: "Reviews",
      description: "Manage product reviews",
    },
  },
  de: {
    reviews: {
      title: "Bewertungen",
      description: "Produktbewertungen verwalten",
    },
  },
}
2

Reference the namespace in your page config

src/routes/reviews/page.tsx
export const config: RouteConfig = {
  label: "reviews.title",
  icon: Star,
  translationNs: "reviews",
}
3

Set the default language

vite.config.ts
mercurDashboardPlugin({
  medusaConfigPath: '../../packages/api/medusa-config.ts',
  name: 'My Marketplace',
  i18n: {
    defaultLanguage: 'en',
  },
})

FAQ

No. The Mercur panels don’t load Medusa’s admin extension system — there are no widget zones to inject into. The equivalent workflows are: drop-in routes for new pages, compound-component re-composition for changing existing pages, and the four components overrides for global chrome. Backend customization (workflows, modules, subscribers) still follows standard Medusa conventions.
Import the page from the /pages subpath (@mercurjs/vendor/pages or @mercurjs/admin/pages), drop a route at the same path, and re-compose its slots — keep the slots you don’t change and they retain all their behavior (data fetching, filters, pagination). The re-compose tutorial walks through a real example.
A sidebar item is only generated when the route file exports a config object with a label. Also check that the file is named exactly page.tsx (or .ts/.jsx/.js) under src/routes/ and has a default export — files without one are skipped entirely.
No — all panel configuration is passed inline to mercurDashboardPlugin() in vite.config.ts. If you’ve seen references to a separate config file, they’re outdated.
Yes — both panels use the same SDK and the same conventions. The differences are which built-in pages exist (@mercurjs/admin/pages vs @mercurjs/vendor/pages) and a few vendor-only options like StoreSetup and enableSellerRegistration.

Next steps

Add a custom panel page

Hands-on: your first drop-in route, end to end.

Re-compose a built-in page

Hands-on: override the vendor products page while keeping its table.

Replace panel components

Hands-on: swap the topbar actions and sidebar.

Build your own block

Package your customization and share it across projects.