@mercurjs/dashboard-sdk. You customize them by dropping files under src/.
Customization is file-based and convention-driven. You add pages, widgets, and field extensions by placing files under src/, and you shape navigation through a single file. Nothing is registered by hand. The SDK crawls your src/ at build time and wires everything in.
defineWidgetConfig, defineRouteConfig, defineCustomFieldsConfig, and createFormHelper, all exist here, re-exported from @mercurjs/dashboard-sdk (without Medusa’s unstable_ prefix). Two things differ. Mercur ships two panels (admin and vendor) from one framework, so a file only ever targets the panel it lives in (there is no surface field), and widget zones carry a before | after placement suffix on the zone id. Everything is additive by default: your contribution augments the built-in page instead of replacing it.The extension mechanisms
Every panel customization is additive. You augment a built-in page without owning it. Each file handles one concern and is discovered by the build-time crawl:- Widgets: inject a React component at a named zone on a built-in page (
defineWidgetConfig). - Navigation: reorder, hide, relabel, or re-parent built-in sidebar items (
defineNavigationConfig). - Custom fields: add validated fields to built-in forms, replace, remove, or add fields in detail sections, and add columns to list tables (
defineCustomFieldsConfigpluscreateFormHelper). - Pages: add a brand-new screen with a drop-in
page.tsxroute.
Choose your extension mechanism
Use this rule of thumb. Injecting UI into an existing page? Use a widget. Adding data to a form or section? Use a custom field. Reshaping the sidebar? Use the navigation file. Adding a whole new screen? Drop in a route.Set up
All configuration lives in the panel app’s Vite config. There is no separatemercur.config.ts.
Register the plugin
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.create-mercur-app ship with this already wired for both panels.Pass environment values explicitly
.env itself. Load environment variables in vite.config.ts (for example with Vite’s loadEnv) and pass them into the plugin options, as the starter template does with backendUrl.Register typed extension targets (once per panel)
@mercurjs/{admin,vendor}/extension-targets. Register them once so the ids resolve in every extension file, with a single ambient reference in your app’s src.@mercurjs/admin/extension-targets instead. With this file present, a typo in a zone, model, field, or nav id fails tsc instead of silently doing nothing at runtime. create-mercur-app ships this file already.Restart after config changes
vite.config.ts) require a dev-server restart.Configuration options
Widgets
A widget injects a React component at a named zone on a built-in page. Drop a file undersrc/widgets/, export the component as the default and a config built with defineWidgetConfig.
<domain>.<view>.<placement>. The placement is the last segment:
before/after: stack your widget before or after the built-in content. Multiple widgets stack in registration order.
WidgetZoneId and generated into each panel’s extension-targets.d.ts. Your editor autocompletes them, and an unknown zone fails tsc. Walk through it end to end in Add a widget.
Navigation
Custom drop-in routes place their own sidebar item viadefineRouteConfig (see Add a page). To reshape the built-in sidebar items, author a single host-owned file, src/_navigation.ts.
idtargets any built-in item, top-level routes or nested children, by its stable id, typed asNavItemId.rankorders an item within its parent,hiddenremoves it from the sidebar,labelandiconrelabel it, andnestedre-parents it (nested: nullpromotes to top level, typed againstNavParentId).- Navigation is a single host-owned file. Installed blocks cannot reorder the sidebar, so it stays one source of truth. It does not change custom routes, which still place themselves via
defineRouteConfig.
Custom fields
A custom field adds validated fields to a model’s built-in create and edit forms, replaces, removes, or adds fields in its detail sections, and adds columns to its list table, all from one model-scoped file. Dropsrc/custom-fields/<model>.tsx and default-export a defineCustomFieldsConfig.
forms[]adds validated fields to a formzone(create,edit,organize, and so on). Input type and validation come from a Zod schema viacreateFormHelper. Fields render through the standardForm.Fieldchain and participate in the existing submit and validation flow.displays[]targets detail-page sections, keyed by fieldid. An entry adds a read-only row (unknown id), replaces a built-in field’s render (matching id pluscomponent), or removes it (matching id pluscomponent: null).listextends the model’s list table: add or override columns by id, hide viaviewDefaults.columnVisibility, reorder viaviewDefaults.columnOrder.- Everything is typed against the panel-generated
CustomFieldsRegistry. Validmodel,zone, and built-in field ids autocomplete, and an invalid target failstsc.
Add a page
Create the route file
page.tsx inside src/routes/ and export a default React component. The route is determined by the file path.Add the config export for navigation
config with a label. Pages without one are still routed. They just don’t appear in the menu.loader (React Router data loader) and handle (route metadata) alongside the default component.Open it in the running panel
/reviews route with a “Reviews” sidebar item. No route registration, no configuration file.src/routes/products/page.tsx maps to /products), your page replaces the built-in one. Any other path is added alongside the built-in routes. Delete the file and the built-in page returns. To change part of a built-in page without owning it, prefer a widget or a custom field.Routing conventions
File paths map to URL routes automatically.Branding
Setname and logo in the plugin options to customize the sidebar header.
Internationalization
Create translation resources
Reference the namespace in your page config
Set the default language
FAQ
Can I use Medusa's defineWidgetConfig and widget zones?
Can I use Medusa's defineWidgetConfig and widget zones?
defineWidgetConfig is re-exported from @mercurjs/dashboard-sdk and drives file-based widgets under src/widgets/. The difference from Medusa is the before | after placement suffix on the zone id and that each panel ships its own typed zone set. See Add a widget.How do I add a field to a built-in form or detail page?
How do I add a field to a built-in form or detail page?
defineCustomFieldsConfig in src/custom-fields/<model>.tsx. forms[] adds validated fields to create and edit forms, and displays[] adds, replaces, or removes fields in detail sections. See Extend forms and tables.How do I change just one part of a built-in page?
How do I change just one part of a built-in page?
Is there a mercur.config.ts file?
Is there a mercur.config.ts file?
mercurDashboardPlugin() in vite.config.ts. If you’ve seen references to a separate config file, they’re outdated.Does this apply to both the admin panel and the vendor portal?
Does this apply to both the admin panel and the vendor portal?
extension-targets.d.ts). Today the widget zones and product custom fields are mounted in the vendor portal, and navigation overrides work in both.Next steps
Add a widget
defineWidgetConfig.Extend forms and tables
defineCustomFieldsConfig.