Why slots exist
The panel’s product edit page ships with no SEO section by design: it is the reference example of what a slot is for. Rather than the panel guessing at every field a store might want on that page, the page exposes named zones at its meaningful seams, and an SEO add-on (or any other) injects a card into one of them. The worked example below is that add-on, drawn fromlunarphp/panel-addon-example.
The zone-naming convention
A zone name has the shape{page}:{region}[:position]:
{page}— the panel route name for the target page, with thepanel.prefix stripped.Lunar\Panel\Http\Middleware\HandlePanelInertiaRequestsderives the current page’s id the same way, from$request->route()->getName(), so a zone only matches if this segment is exactly right.{region}— a named slot inside that page’s Vue template, declared by a<PageZone region="..." />.[:position]— an optional qualifier the page template defines, almost alwaysbeforeorafter.
Registering a slot
Add a slot from aSection’s (or SectionExtension’s) slots() hook:
Lunar\Panel\Slots\Slot takes:
Slot ordering is a plain ascending
priority integer, not the before/after anchor-capable Lunar\Panel\Support\Position primitive used by navigation, table columns, and actions (see Ordering with Position). Two slots in the same zone with equal priority keep registration order.Section, call Panel::slots()->add(new Slot(...)) directly via the Lunar\Panel\Facades\Panel facade.
How the record prop flows
Where a page’s zone sits next to a record, the page passes that record down as a prop on its<PageZone> tag, for example the product edit page:
Lunar\Panel\Http\Middleware\HandlePanelInertiaRequests shares the resolved slots for the current page as the slots Inertia prop; PageZone.vue computes the zone name from the page id and forwards any extra attributes (:product="product" becomes an $attrs entry) to PanelSlot.vue, which binds them onto the resolved component after the slot’s own static props:
props plus whatever record the page zone carries — the page’s record prop wins if a key collides with a static one, though in practice they use different names. A component only reads the props it needs; it doesn’t have to declare product if it doesn’t use it.
Registering the component
Register the Vue component at the top level of the add-on’s compiled bundle, namespaced under the add-on’s own key, never insidewindow.LunarPanel.booting() (whose callbacks run after the panel’s first render — too late for a component a slot needs on first load):
Worked example: an SEO card
src/ExampleSection.php in lunarphp/panel-addon-example registers two slots — a plain banner on the Customers edit page, and the canonical SEO card on the product edit page:
resources/js/components/SeoCard.vue reads the product prop the zone passes down:
Known first-party zones
Every zone a first-party page currently exposes, found by searching for<PageZone under packages/panel/resources/js/pages/. Where a page has no bound record (a listing or create page), its zones receive no extra props; where it does, the props passed are listed.
The product edit page’s
content:after zone (between the Basics/Media/Attributes cluster and the variants block) is the one the SEO card walkthrough above targets — it is the intended home for a content-adjacent card like SEO. New zones are added to first-party pages over time; re-run the same search against a current checkout of packages/panel/resources/js/pages/ to confirm a zone still exists before relying on it.
See also
- Extending Tables — the equivalent mechanism for adding columns, filters, and actions to a table instead of injecting a component into a page.
- Page Actions — adding a header action rather than body content.
- Ordering with Position — the shared placement primitive used elsewhere in the panel (not slots, which use a plain
priorityinteger).