Registering blocks
A block is an ordinary Laravel Blade class component - Illuminate\View\Component - that implements WeArePixel\Inlay\Contracts\InlayBlock, a marker interface with no required methods. Nothing downstream (the registry, the picker, the settings inspector, rendering, publishing, revisions) treats a block differently based on how its settings were declared.
Class + view blocks
The only requirement is the marker interface and a render() method returning a view, exactly like any other Blade class component:
namespace App\Inlay\Blocks;
use Illuminate\View\Component;
use Illuminate\View\View;
use WeArePixel\Inlay\Contracts\InlayBlock;
final class Hero extends Component implements InlayBlock
{
public function __construct(
public string $heading = 'Welcome',
) {}
public function render(): View
{
return view('blocks.hero');
}
}
With no attributes and no settings() method, this block registers and renders fine - it simply has zero editable settings. The heading constructor parameter is never exposed to the editor unless you explicitly declare it as a setting (see Attribute settings and Fluent settings): an injected repository, a service, or any other constructor dependency is left entirely alone and resolved by the container as usual when the block renders. This is deliberate - a constructor parameter is never an editor setting by accident.
Registering a handle
Register the class against a 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, so renaming or moving the class doesn't break existing pages.
Categories and picker metadata
An optional #[BlockMeta] class attribute controls how a block appears in the admin's block picker:
use WeArePixel\Inlay\Attributes\BlockMeta;
#[BlockMeta(
label: 'Hero',
category: 'Headers',
description: 'A large banner with a heading and image.',
icon: 'layout-header',
thumbnail: null,
keywords: ['banner', 'header', 'landing'],
)]
final class Hero extends Component implements InlayBlock
{
// ...
}
Every field is optional. With no #[BlockMeta] at all, the picker falls back to a label derived from the block's registered handle (Str::headline()) and groups it under "Uncategorised". keywords are matched by the picker's search alongside the label, description, and category - useful for a term an editor might search for that doesn't appear in the label itself (e.g. "banner" for a block called "Hero").
Picker thumbnails
icon and thumbnail both control the picker card's visual, in order of preference: a thumbnail (a public URL to a real image, e.g. asset('images/blocks/hero.png')) is shown first if set; otherwise icon is shown as a short label; with neither set, the picker falls back to the first letter of the block's label.
#[BlockMeta(
label: 'Hero',
category: 'Headers',
thumbnail: asset('images/blocks/hero.png'),
)]
There's no required size or aspect ratio - a small screenshot of the block's own rendered output works well. A thumbnail is worth the extra asset for a block whose visual identity matters (a hero, a gallery layout); it's what makes the picker feel like a real design tool rather than a list of names.
Category is not a permission boundary
category is presentational only - it groups cards in the picker, and says nothing about which blocks a given page may contain. If you need to actually restrict which blocks a kind of page allows (or require/lock specific ones), that's a Page Definition, a separate concept.
Nested Blade and Livewire usage
A registered block is a normal Blade class component - it may freely contain Livewire, Alpine, or any other host-owned frontend behaviour in its own view, with zero special handling required:
{{-- resources/views/blocks/contact-section.blade.php --}}
<section>
<h2>{{ $heading }}</h2>
<livewire:contact-form />
</section>
Inlay renders every block through a genuine, compiled Blade component tag - not manual new $class(...) construction, and not <x-dynamic-component> - which is exactly what lets a nested <livewire:contact-form /> mount and hydrate with zero special handling, identically whether it's rendered by the admin's preview or by <x-inlay::render> on a real page.
Presentation stays host-owned
Inlay never generates or injects markup into a block's own output - it resolves settings and constructs your block; your block's own Blade view decides everything about structure, classes, and conditional rendering. A setting's value can change which branch your view takes:
<img
src="{{ $image }}"
class="{{ $imageSize === ImageSize::Small ? 'max-w-md' : 'w-full' }}"
>
There's no mechanism for an editor to inject arbitrary CSS classes or markup that your block's own view didn't already provide for.
Common registration errors
| Situation | Exception |
| --- | --- |
| A settings()/attribute entry names a field with no matching constructor parameter | Block [...] declares a setting [...] with no matching constructor parameter. |
| A required, non-nullable constructor parameter has no default | Block [...] setting [...] has no constructor default and is not nullable. ... |
| ->options(EnumClass::class) doesn't match the constructor parameter's actual enum type | Block [...] setting [...] declares options for [...], but the constructor expects [...]. |
| The same setting name is declared twice in one Settings definition | Duplicate setting [...] declared twice in the same settings definition. |
| A Settings modifier (->label(), ->required(), etc.) is called before any field | ...() must follow a field definition. |
| ->options(...) is given a class-string that isn't an enum | [...] passed to options() is not an enum. |
All of these are raised the moment the block is resolved - the first time it's registered and used, not silently deferred to when an editor happens to touch that field. See Single-file blocks for the additional errors specific to that authoring style.