Header, HeaderActions, and DataTable. That means you never fork a page to change one part of it: you drop a page.tsx at the same route path, render the original page, and swap only the slot you care about. Everything you don’t touch keeps its default behavior — data fetching, filters, pagination, i18n, all of it.
This is not Medusa’s widget system. Medusa’s admin lets you inject widgets into predefined zones around a page. Mercur takes a different approach: pages are React compound components you re-compose directly. There are no injection zones and no
defineWidgetConfig — you get the actual page component and decide what renders inside it. See Extending Panels for the full comparison.What you’ll build
A vendor portal/products page with Import and Export buttons added next to the built-in Create button — while the table, search, filters, and everything else stay exactly as shipped. This is the same technique the official product-import-export block uses.
Slot anatomy
Built-in pages are exported from the/pages subpath of each panel package:
| Slot | Renders |
|---|---|
Table | The Container shell holding header + table |
Header | The title/actions row |
HeaderTitle | Heading and subtitle |
HeaderActions | The action button cluster |
HeaderCreateButton | The built-in Create button |
DataTable | The wired data table — fetching, columns, filters, pagination |
CustomerDetailPage.Main, CustomerDetailPage.Sidebar, CustomerDetailPage.MainGeneralSection).
Override the page
Drop a route file at the same path
Create the file in your vendor app at the path that matches the built-in route.
/products maps to src/routes/products/page.tsx:apps/vendor/src/routes/products/page.tsx
Reuse the slots you don't change
Note what happened in that file:
HeaderTitle, HeaderCreateButton, and DataTable are the originals, rendered untouched. Only HeaderActions gained two buttons. You re-compose top-down — render the page, re-declare only the branch you’re changing, and keep original slots for everything inside it.When a custom route’s path matches a built-in route, your page replaces the built-in one. Routes at new paths are appended instead. That one rule is the entire override mechanism.
How defaults work
If you render a compound page with no children, it renders its full default composition — so<ProductListPage /> alone is identical to the built-in page. Every page root follows the pattern:
Verify
- Run your project (
bun run dev) and open the vendor portal. - Navigate to Products — the list should look identical to before, plus Import and Export buttons in the header.
- Search, filter, and paginate the table — all built-in behavior must still work, since
DataTableis untouched. - Delete your
src/routes/products/page.tsxand reload — the built-in page comes back. Nothing was forked.
FAQ
Where do I find which slots a page exposes?
Where do I find which slots a page exposes?
Import the page from
@mercurjs/vendor/pages or @mercurjs/admin/pages and let your editor’s autocomplete list the attached members — every slot is a static property on the page component. The naming is consistent across pages: list pages expose Table/Header/HeaderTitle/HeaderActions/HeaderCreateButton/DataTable; detail pages expose Main/Sidebar plus one property per section.Can I remove a built-in element, like the Create button?
Can I remove a built-in element, like the Create button?
What about changing the table columns?
What about changing the table columns?
DataTable is a single slot — you either keep it wholesale or replace it with your own table. For a different column set, replace the slot with your own component built on the shared DataTable/useDataTable primitives from the panel package.Next steps
Replace panel components
Swap global chrome like the sidebar or topbar instead of a single page.
Add a custom API route
Back your custom page with a typed endpoint of your own.