Skip to main content
Wintertrace
Browse the documentation

Building modules

Template slots

Inject or replace markup at defined points in core views from a module, using the slot registry — without editing core templates.

Template slots are predefined injection points in the application’s layouts. A module can output its own Blade content at these points without touching the core views. That is the essential point: you extend the UI without editing files that would be overwritten on the next update — keeping upgrades conflict-free.

How slots work

A slot has two halves: the core layout marks the location, and your module registers content for it.

In Blade layouts (core)

The layout contains a directive at the designated location:

@extensionSlot('admin.content.after')

This directive internally calls SlotRegistry::render('admin.content.after', $user) and outputs the collected HTML. You do not write this directive yourself — it already exists in the core layouts. Your only task is to register content for the appropriate slot name.

In your module (registration)

Registration goes through the SlotRegistry, resolved in your ServiceProvider’s boot():

$slots = app(SlotRegistry::class);

// Append content (multiple modules can append to the same slot)
$slots->append('admin.content.after', 'my-module::my-partial', ['key' => 'value']);

// Or replace a slot entirely (last-wins)
$slots->replace('admin.content.before', 'my-module::replacement');

The distinction between append and replace is intentional: append is additive and cooperative — any number of modules can contribute to the same slot. replace substitutes the entire slot content and should be used sparingly.

Available slots

The slots available depend on which layout is in use. Three layouts provide injection points: the admin layout, the driver layout, and the portal layout.

Admin layout (layouts/admin.blade.php)

Slot nameLocationUse case
admin.head.afterInside <head>, after core CSSCustom stylesheets, meta tags
admin.sidebar.before-navAbove navigation in sidebarModule branding, notices
admin.sidebar.after-navBelow navigation in sidebarAdditional links, status info
admin.content.beforeBefore main content areaBanners, warnings
admin.content.afterAfter main content areaFooter widgets, debug info

Driver layout (layouts/driver.blade.php)

Slot nameLocationUse case
driver.head.afterInside <head>, after core CSSCustom stylesheets
driver.topbar.actionsIn top bar, action areaQuick action buttons
driver.content.beforeBefore main contentNotices, alerts
driver.content.afterAfter main contentAdditional features
driver.bottom-nav.afterAfter bottom navigationExtra nav items

Portal layout (layouts/portal.blade.php)

Slot nameLocationUse case
portal.head.afterInside <head>, after core CSSCustom stylesheets
portal.nav.afterAfter portal navigation itemsAdditional nav links
portal.content.beforeBefore main contentNotices, seasonal info
portal.content.afterAfter main contentAdditional sections
portal.footer.beforeBefore footerLegal notices, links

Slot API

Appending with append

Multiple modules can append to the same slot. Content is rendered in order, controlled by the order parameter:

$slots->append(
    slotName: 'admin.content.after',
    viewPath: 'my-module::partials.info-panel',
    data: ['message' => 'Hello from module'],
    order: 100,           // sort order (lower = first)
    permission: 'settings.view', // optional: only render if user has this permission
);

order determines where your content appears in the slot when other modules also contribute — a lower value moves it earlier. This keeps the ordering predictable even when you do not know which other modules are active.

Replacing with replace

replace substitutes the entire slot content. If multiple modules replace the same slot, the last one registered wins — and a warning is logged. Use replace sparingly; the warning is your signal that two modules are conflicting.

$slots->replace(
    slotName: 'admin.sidebar.before-nav',
    viewPath: 'my-module::sidebar-header',
    data: [],
    permission: null,
);

Permission gating

If a permission is specified, the slot content is only rendered for users who have that permission. This restricts content to specific roles without repeating the check inside the view itself:

$slots->append('admin.content.after', 'my-module::admin-only', [], 100, 'settings.edit');
// Only visible to users with 'settings.edit' permission

Which permissions exist and how a module registers its own is covered in Permissions & roles.

Example: adding a banner

In your ServiceProvider’s boot(), register the partial …

// In your ServiceProvider boot()
$slots = app(SlotRegistry::class);
$slots->append('admin.content.before', 'my-module::banners.update-available', [
    'version' => '2.0.0',
], 50);

… and the corresponding Blade view renders the actual content:

{{-- resources/views/banners/update-available.blade.php --}}
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
    <p class="text-blue-800">
        Module-Update {{ $version }} verfügbar.
    </p>
</div>

Values passed via data are available in the view as variables — here $version.

Example: adding a tab to the portal

Often two slots work together: one for the navigation link, one for the content it reveals. To add a dedicated section to the portal:

// Append navigation link
$slots->append('portal.nav.after', 'my-module::nav.documents-link');

// Append content section
$slots->append('portal.content.after', 'my-module::portal.documents-section');

Slots are just one of several extension points a ServiceProvider registers. How the SlotRegistry fits alongside the other registries in a module’s boot sequence is shown in The ServiceProvider pattern; the full registry API is in Registries.