Docs / Inlay / Registering Blocks

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.

Discovery Call

One call.
We'll both know.

20 minutes to walk through your project, ask the hard questions, and work out honestly if we're the right team for it.

  • One call per day - it gets our full attention
  • Australia's only Laravel Premier Partner
  • Senior engineers only - no juniors on your project
  • Brisbane-based, onshore team

Press Esc to close  ·  B to reopen