Skip to main content
A slot renders an add-on’s own Vue component inside a first-party (or another add-on’s) panel page, at a spot that page has deliberately exposed — without the target page knowing the add-on exists.

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 from lunarphp/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 the panel. prefix stripped. Lunar\Panel\Http\Middleware\HandlePanelInertiaRequests derives 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 always before or after.
Zone prefixes come from the target page’s actual route name, not from what the page conceptually does. The Customers edit page is routed as panel.customers.edit — there is no separate panel.customers.show — so the correct zone prefix is customers.edit. A slot registered against a prefix that matches no page’s route simply never renders: Lunar\Panel\Slots\SlotRegistry::forPage() finds no match, and nothing logs or throws. If a slot isn’t appearing, check this first.

Registering a slot

Add a slot from a Section’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.
Outside a 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:
So a slot component receives its registered 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 inside window.LunarPanel.booting() (whose callbacks run after the panel’s first render — too late for a component a slot needs on first load):
See Building an Add-on for the full bundle setup.

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:
A real SEO add-on would persist its fields through its own registered routes; the example card keeps its state local to stay a pure slot demonstration.

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 priority integer).