Appearance
A Livewire full page component layout that matches your site
A Livewire full page component layout is normally one Blade file you maintain by hand, and on a site where the marketing pages are built in a visual editor, that file drifts from the real header and footer within a week. In FilamentCraft you point the component at the package's layout instead, with #[Storefront] on the class, and your cart, checkout or account page renders inside the same theme tokens, header region, footer region and fonts as every page an editor built. This tutorial builds a cart page that way, then covers the three other ways to reach the same shell and the gotchas we hit on the way.
The problem with a hand-written layout
Livewire renders a full page component inside a layout view. The Livewire 3 docs describe the default as "your application's layout, typically defined in the resources/views/components/layouts/app.blade.php file", and let you override it per component with the #[Layout] attribute or globally with the layout key in config/livewire.php.
That works until the header is content. On a FilamentCraft site the header and footer are regions: stacks of sections an editor composes in the page editor and saves once for the whole site. The colors come from the site's color scheme, the fonts from its theme, and on a multi-tenant app every tenant has different ones. A static app.blade.php cannot know any of that. You end up rendering a second, simplified header for the pages that are not built in the editor, and visitors notice the jump when they go from the home page to the cart.

A Livewire full page component layout in one attribute
The fix is to render your component inside the same shell the package uses for built pages. For a Livewire full page component, that is one attribute:
php
<?php
namespace App\Livewire;
use FilamentCraft\Attributes\Storefront;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Storefront]
#[Title('Your cart')]
final class CartPage extends Component
{
public function render(): View
{
return view('livewire.cart-page');
}
}Storefront is not magic. It extends Livewire's own Livewire\Attributes\Layout and passes the view name filamentcraft::layout to the parent constructor, so everything Livewire does with #[Layout] works the same. If you would rather stay with the attribute your team already knows, #[Layout('filamentcraft::layout')] does exactly the same thing. #[Title] keeps working because it is Livewire's attribute, not ours.
The route needs no middleware and no wrapper view:
php
// routes/web.php
Route::get('/{tenantSlug}/cart', CartPage::class);The component view is just the page body. There is no <html>, no <head> and no header include:
blade
{{-- resources/views/livewire/cart-page.blade.php --}}
<div class="mx-auto max-w-5xl px-6 py-12">
<h1 class="fc-title">Your cart</h1>
@foreach ($lines as $line)
{{-- your cart rows --}}
@endforeach
</div>fc-title is the fluid heading class the built-in sections use since v1.40.3, so your heading matches theirs. Use your own Tailwind classes for everything else.
What the layout renders around your component
The filamentcraft::layout view is backed by the FilamentCraft\View\Components\Layout Blade component. It builds the same shell the page renderer builds for a template:
- the site's theme tokens as CSS custom properties, so your markup can read
--fc-*variables; - the header region above your slot and the footer region below it;
- the registered stylesheets and fonts;
lang,diranddata-fc-color-schemeon<html>, so an Arabic site getsdir="rtl"on your cart page too.
It also renders Livewire's styles and scripts. The livewire prop defaults to true, and you set it to false only for a fully static page.
How the layout finds the right site
A layout that renders a tenant's header has to know which tenant. The component resolves the Site in this order:
- an explicit
siteprop; - a
Sitealready bound in the container, which is what the package middleware does; TenancyResolver::resolve(), which covers single-site config and the Filament tenant inside a panel;- the tenant slug in the current URL, looked up against the configured owner model.
That fourth step is why the bare route above works. When the URL has a {tenantSlug} parameter and filamentcraft.tenancy.owner_model is set, the layout loads the owner by slug, finds its live site and binds it. If all four fail, it throws a RuntimeException that names the three ways to fix it, rather than rendering a page with no theme.
Three other ways to get the same shell
#[Storefront] fits Livewire-first apps. The package ships three more entry points, and they all end in the same Blade component, so you can mix them.
A route group, when you have several storefront routes and want tenancy wired once:
php
Route::filamentCraftStorefront(\App\Models\Academy::class, function (): void {
Route::get('cart', CartPage::class);
Route::get('checkout', CheckoutPage::class);
Route::get('account', AccountPage::class);
});The macro registers the group with the {tenantSlug} prefix and the web and filamentcraft.tenant middleware. With filamentcraft.tenancy.owner_model in config, the owner class argument is optional: Route::filamentCraftStorefront(routes: function (): void { ... }).
File-based pages with Laravel Folio, scaffolded by php artisan filamentcraft:install --folio. The flag writes a starter resources/views/storefront/cart.blade.php and prints the Folio::path() registration to paste into your FolioServiceProvider. It does not run folio:install for you.
And the Blade component directly, for a controller view or a one-off page:
blade
<x-filamentcraft::layout :title="'Your cart'">
@livewire('cart-items')
</x-filamentcraft::layout>The component class is registered under the package namespace, and you can alias it to your own name with Blade::component('academy-shell', \FilamentCraft\View\Components\Layout::class).
Gotchas we hit
Route order. Route::filamentCraftTenant(), the macro that serves the editor-built pages, is a wildcard: {tenantSlug}/{path?}. Register your cart, checkout and account routes before it, or the wildcard catches them and looks for a template called cart.
Utility pages are noindex by default. The component's indexable prop defaults to false, because cart, checkout and account pages have no business in search results. For a host-rendered page you do want indexed, a searchable course catalog for example, pass :indexable="true". The layout still emits a canonical URL for the current request either way.
The color scheme follows the site. Unless you pass color-scheme, the shell uses the site's global scheme, the same one every template renders on. Before v1.31.0 host pages always rendered on the package default, whatever scheme the operator had picked for the site. Pass a scheme slug only when one page should deliberately look different.
A new site has no header yet. A freshly created Site has no region rows, and the region renderer outputs nothing for a missing region. Your cart page renders correctly, with no header bar, until someone builds the header in the editor or you seed it from a blueprint.
Inside a Filament panel, none of the tenant wiring is needed, because Filament::getTenant() feeds the resolver. On public routes you need the middleware, the route macro, or a URL parameter that matches filamentcraft.tenancy.url_parameter (default tenantSlug).
Where this fits
Use the visual editor for pages whose layout changes with marketing, and a Livewire full page component for anything driven by the visitor's state: a cart, a lesson player, search results. The shell is what makes the two read as one site.
The dynamic pages guide has the full prop table and the Folio setup, and tenancy explains the resolver chain in detail. To see built pages and host pages side by side, open the live demo.
