Skip to main content
Every panel extension comes from an ordered list of layers:
  1. @mercurjs/admin or @mercurjs/vendor itself,
  2. each plugin in medusa-config plugins order, from its built .medusa/server/src/<admin|vendor>/index.mjs,
  3. your Medusa project’s src/admin or src/vendor folder (next to medusa-config.ts),
  4. the panel app’s own src folder.
A later layer wins. A route at a path an earlier layer declared replaces it, a widget with the same id replaces it, a translation key overrides it, and a layout preference overrides it field by field. Every layer uses the same folders: routes/, widgets/, custom-fields/, layouts/, i18n/ and a _layouts.ts file.

Pages expose named entries

A detail or list page renders a LayoutComposer. It names the zone widgets target, the layout it uses, and gives each section a stable entry id:
The page’s own sections become entries core:ProductGeneralSection, core:MetadataSection and so on; widgets become widget:<id>. Use the same component on your own pages and the build emits their zones and entry ids into extension-targets.d.ts, so the next layer gets autocompletion for them.

Place a widget

zone is the page prefix or one of its sections. section and order place the widget among the page’s entries; core entries sit at order 0, so a negative order renders before them and no order renders after.
replace takes the slot of a page entry. The widget receives renderDefault to render what it replaced:
The legacy product.detail.side.before and .after zones still work and map to the first and last position of that section.

Hide, reorder and move entries: _layouts.ts

A layer’s _layouts.ts holds per-zone preferences that apply to everyone. The sidebars are zones too (nav.main, nav.settings) with nav:<path> entries, which replaces _navigation.ts.

Override a page

A page.tsx at a path an earlier layer already serves replaces it. The component receives renderDefault, which renders the page it replaced:

Block the panel behind a flow

There is no app-wrapper seam, as in Medusa. A flow every member must complete before using the panel (terms acceptance, a verification step) is a page plus a redirect: a routes/<flow>/page.tsx with config.public = true so it renders in the bare public layout, and a widget on the topbar zone that checks the status with useQuery and navigates to the page while the flow is outstanding. The API keeps enforcing the rule with a 403 for everything else.

Register a layout

A file under layouts/ registers a layout other pages can pick. Augment LayoutSectionRegistry so the sections prop is typed.