Installation
This walks through a fresh composer require to publishing your first page, using a Hero block as the running example. It assumes an existing Laravel 11, 12, or 13 application.
1. Install the package
composer require wearepixel/inlay
composer require "livewire/livewire:^4.0"
Inlay's packaged admin is built on Livewire ^4.0, and Livewire is not a dependency Laravel ships with - install it alongside the package.
It's deliberately a suggest rather than a require, because rendering a composed page (<x-inlay::render>, Inlay::renderPage(), Inlay::pageRoutes()) never touches Livewire at all: a host application that only renders can skip it entirely. If you skip it and later call Inlay::adminRoutes() anyway, that call throws immediately and tells you so. Already on Livewire 3? Inlay conflicts with it - the admin resolves its own components through Livewire 4's namespaced component resolution, which Livewire 3 has no equivalent for.
2. Run the installer
php artisan inlay:install
This publishes config/inlay.php and runs php artisan migrate, creating the pages, page_sections, and page_revisions tables. If you'd rather do these individually:
php artisan vendor:publish --tag=inlay-config
php artisan migrate
3. Register the routes
Inlay registers no routes on its own, of any kind. You opt into each part explicitly, from your own routes/web.php:
use WeArePixel\Inlay\Facades\Inlay;
Inlay::adminRoutes(); // the admin - page index, editor, preview
Inlay::pageRoutes(); // published Pages, served publicly by Inlay itself
Inlay::routes(); // both of the above, as a convenience
adminRoutes() registers the admin under config('inlay.admin.prefix') (inlay by default, so /inlay/pages, /inlay/pages/create, and so on) with route names prefixed inlay.admin.. A bare visit to the prefix root (e.g. /inlay) redirects to the page index rather than 404ing. Visit /inlay/pages and you should see an empty page index - there's nothing to compose yet, because no block is registered.
Most applications need adminRoutes() plus either pageRoutes() or their own routes calling Inlay::renderPage() - rarely both, and never neither. See Public routing for the full reference.
4. Set your admin middleware
Open config/inlay.php:
'admin' => [
'middleware' => ['web'],
],
Stop here before going further if your application has real content or real users. ['web'] alone means anyone who can reach your application can create, edit, and delete pages - there is no authentication or authorization built into the package itself, by design. The package is deliberately auth-agnostic; your application owns that decision. At minimum:
'admin' => [
'middleware' => ['web', 'auth'],
],
If your application uses Laravel's default auth guard and has a login named route, this is enough to require a logged-in user. If not every authenticated user should be able to manage pages, layer in your own authorization:
'admin' => [
'middleware' => ['web', 'auth', 'can:manage-pages'],
],
where manage-pages is a Gate::define() or policy ability your application defines - Inlay has no opinion on how you express it, it just needs a middleware name Laravel already understands.
This is entirely separate from config('inlay.pages.middleware'), which guards public Page requests instead - a visitor never needs an admin session to view a published Page, and an admin session is never required to serve one.
Why Livewire middleware persistence matters
The admin's every action after the first page load - adding a block, editing a setting, publishing - is dispatched to Livewire's own /livewire/update endpoint, not back through Inlay::adminRoutes(). Middleware that isn't explicitly told to persist across that boundary would otherwise guard only the initial page load, leaving every subsequent action unprotected for a session that's since been revoked or expired.
Inlay::adminRoutes() handles this for you: it resolves config('inlay.admin.middleware') to concrete middleware classes and registers them with Livewire as persistent middleware, so they re-run on every subsequent admin action too. You don't need to configure this separately - just make sure your real middleware is in config('inlay.admin.middleware') before Inlay::adminRoutes() runs.
5. Write your first block
A block is a normal Blade class component that implements InlayBlock - a marker interface with no required methods:
// app/Inlay/Blocks/Hero.php
namespace App\Inlay\Blocks;
use Illuminate\View\Component;
use Illuminate\View\View;
use WeArePixel\Inlay\Attributes\BlockMeta;
use WeArePixel\Inlay\Attributes\Text;
use WeArePixel\Inlay\Contracts\InlayBlock;
#[BlockMeta(label: 'Hero', category: 'Headers', description: 'A page heading with introductory copy.')]
final class Hero extends Component implements InlayBlock
{
public function __construct(
#[Text(label: 'Heading', maxLength: 80)]
public string $heading = 'Welcome',
#[Text(label: 'Subheading')]
public string $subheading = '',
) {}
public function render(): View
{
return view('blocks.hero');
}
}
{{-- resources/views/blocks/hero.blade.php --}}
<section class="hero">
<h1>{{ $heading }}</h1>
@if ($subheading)
<p>{{ $subheading }}</p>
@endif
</section>
Register it by handle in config/inlay.php:
'blocks' => [
'hero' => \App\Inlay\Blocks\Hero::class,
],
The handle (hero) is what gets persisted against a page's sections, never the class name directly - renaming or moving the class later doesn't break existing pages. Reload /inlay/pages and confirm the config picked it up:
php artisan config:clear # only needed if config is cached in this environment
6. Configure the preview
The editor's preview pane is a real iframe pointed at your own frontend. Tell Inlay which of your views renders a composition:
// app/Providers/AppServiceProvider.php
use WeArePixel\Inlay\Facades\Inlay;
public function boot(): void
{
Inlay::previewView('inlay-previews.default');
}
{{-- resources/views/inlay-previews/default.blade.php --}}
<x-app-layout>
{{ $composition }}
</x-app-layout>
Your view receives:
$page- theWeArePixel\Inlay\Models\Pagebeing previewed.$composition- anIlluminate\Contracts\Support\Htmlablecontaining the rendered blocks. Echo it directly;{{ $composition }}is safe, since it's already-rendered HTML rather than raw user input.$context- reserved for future use; currently alwaysnull.
If you skip this step, the preview pane still works - it falls back to a plain, clearly-labeled unstyled wrapper so you always know it isn't your real design yet.
7. Create and publish your first page
- Visit
/inlay/pagesand click New page. Give it a title. A public path is optional - leave it blank if you'd rather render this page through a route your own application already owns (option B below). - In the editor, click + Add, choose Hero from the picker, and edit its heading in the inspector.
- Watch the preview pane update.
- Click Publish.
8. Render it on your actual site
Pick one of two options - see Public routing for the full reference.
Option A - let Inlay serve it publicly. Give the page a public path (e.g. about) in step 7, then register Inlay::pageRoutes():
// routes/web.php
Inlay::pageRoutes(prefix: 'pages'); // -> /pages/about
{{-- resources/views/inlay/pages/show.blade.php --}}
<x-app-layout>
{{ $composition }}
</x-app-layout>
Configure this view path with config('inlay.pages.view') if you'd rather use a different name.
Option B - render it through a route you already own:
// routes/web.php
use WeArePixel\Inlay\Models\Page;
Route::get('/{page:path}', function (Page $page) {
return view('pages.show', ['page' => $page]);
});
{{-- resources/views/pages/show.blade.php --}}
<x-app-layout>
<x-inlay::render :page="$page" />
</x-app-layout>
Either way, visit the page's URL and you should see the published Hero block, styled by your own application's CSS - not Inlay's.
Next steps
- Registering blocks - the full block-authoring reference.
- Public routing - prefixes, nested paths, the homepage,
Inlay::url(). - The editor - the editor-facing workflow in plain language.
- Current limitations - what this release deliberately doesn't include yet.