Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mason Laravel Package

awcodes/mason

Mason is a Filament plugin for composing flexible, block-based content with draggable bricks. Build rich layouts from reusable blocks, store them as JSON, and render them anywhere in your Laravel app. Ideal for pages, posts, and landing pages that need editor-friendly structure.

View on GitHub
Deep Wiki
Context7

Getting Started

First, disambiguate the package. The Composer name says one thing, the README one-liner says another. awcodes/mason is a Filament form field for block-based page/document building — drag-and-drop editor, per-block config forms, iframe preview, server-side Blade rendering. It is not a CSS masonry/waterfall grid component, despite what the short repository description implies. Confirm against the installed artifact before you plan anything: drop Mason::make('content') into a form, register one brick, render one entry front-end. That spike is the cheapest hour you will spend.

Install and register.

composer require awcodes/mason
php artisan vendor:publish --tag=mason-assets   # verify the exact tag from the docs
php artisan filament:assets

Register the plugin on the panel you actually want the editor in — not globally — so the field and its assets stay scoped to the admin surface:

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(MasonPlugin::make())
        // ...
}

Where to look first: the security page (https://docs.aw.codes/mason/3.x/security) before the getting-started guide. It defines the trust boundary you are about to inherit — Mason trusts its signed render context and its own config; brick authors must treat brick configuration as untrusted input. Read that, then read the brick contract docs, then the field docs.

Data model. Mason content is structured JSON in an existing column. No new tables to migrate, no custom storage engine:

Schema::table('pages', fn (Blueprint $t) => $t->json('content')->nullable());
protected $casts = ['content' => 'array'];

First use case — a marketing page with a fixed menu of blocks. Model it as three or four brick classes owned by your team (hero, CTA row, media+text, testimonial row), each with a config form and a Blade view. Resist the urge to start with a general-purpose "text block"; the buy case for Mason is structured and repeatable content, and a generic rich-text brick is a more expensive way to reach for Tiptap.

Rendering published output. Two Blade directives, and both belong in your layouts, not in random partials:

{{-- resources/views/app.blade.php --}}
<head>@masonEntryStyles</head>

If you have a custom preview layout for the editor iframe, it takes @masonStyles instead. Style resolution is render-time as of 3.6.0, so style updates no longer require clearing compiled views — but see the gotchas below for the one-time view:clear after upgrading.


Implementation Patterns

One brick per block type, and keep bricks dumb. A brick should be: an identifier, a Filament schema for its config, a toHtml($data) that renders a Blade view, and nothing else. No queries inside toHtml(), no service container resolution, no auth checks. Bricks are rendered in a preview loop where no record necessarily exists — the moment you assume a persisted model, preview breaks and you will be debugging it through an iframe.

// Illustrative shape — confirm the exact contract against the brick docs/generator.
class HeroBrick extends Brick
{
    protected string $name = 'hero';

    public function form(): array
    {
        return [
            TextInput::make('heading')->required()->maxLength(120),
            Textarea::make('subheading')->maxLength(240),
            FileUpload::make('image')->image(),
            TextInput::make('cta_url')->url(),
            TextInput::make('cta_label')->maxLength(40),
        ];
    }

    public function toHtml(array|null $data = null): string
    {
        return view('bricks.hero', ['config' => $this->getConfig()])->render();
    }
}

Escape at the brick boundary, not in the layout. Because config is untrusted input by contract, each brick view owns its own posture. Build a habit: every string out of config goes through {{ }} (never {!! !!}), every URL through an allowlist check on scheme (http/https only — reject javascript: and data:), every embed through a domain allowlist. If a brick genuinely needs raw HTML, that is a deliberate decision with a sanitizer (mews/purifier or symfony/html-sanitizer) in front of it, documented in the brick, and probably gated behind a permission.

Use previewData() to make preview honest — sparingly.

Mason::make('content')
    ->previewData(fn (?Post $record): array => [
        'language' => $record?->language_id,
        'tenant'   => $record?->tenant_id,
    ]);

Two rules, both non-negotiable: source the payload from the saved record, not a live form field (the editor reads it once — a value that changes between Livewire renders produces a preview that silently stops matching the form), and treat it as a public data channel (it is JSON-serialized into the signed render context and shipped to the browser). Allow-list fields explicitly. Never $record->toArray().

Sequence the brick audit before enabling previewData(). Any brick that distinguishes editor from front-end with $data === null needs rewriting to inspect the data itself. This fails silently — wrong output, no exception, no log line. Grep your brick sources for null checks on the data argument first.

Content schema discipline. Brick identifiers and config keys are persisted in JSON. Renaming a brick class or a config key is a data migration, not a refactor. Keep identifiers stable and slugged; when you retire a brick, keep a fallback renderer (or a retiredBricks map) so historical records don't render as blank holes. Write the migration that rewrites old keys before you ship the renamed form.

Validate config like any other user input. Mason gives you Filament form components per brick; those components' ->rules() and ->maxLength() are your only defence between an editor and malformed JSON in your database. Put the same constraints in the brick's form as you would in a normal Filament resource — they do not come for free.

Local workflow. After any Mason version bump: composer update → php artisan filament:assets → php artisan view:clear (once). Verify the second step in the build log, not in your terminal — see gotchas.

Testing. Don't try to test the drag-and-drop UI with Dusk; test the two things that can actually hurt you: (1) that each brick renders expected HTML given a fixture config, and (2) that each brick neutralises a hostile config (javascript: URL, <script> in a heading, oversized payload). A Pest dataset over your brick registry gives you a cheap regression net that catches new bricks added later.

Integration tip — keep the field in a dedicated resource. Give the page builder its own Filament resource with a full-width layout, rather than embedding Mason::make() in an existing SEO-laden resource form. The editor is tall, drag-heavy, and re-renders constantly; mixing it with unrelated fields makes the whole page sluggish and makes dirty-state debugging harder.


Gotchas and Tips

The asset republish is the release. The postMessage origin-validation fix (3.6.0, #72) ships in the compiled Alpine bundle. composer update alone leaves a patched backend serving a vulnerable front end — the upgrade looks applied and the vulnerability is still live. Treat a missing php artisan filament:assets in the deploy log as a failed release, not a warning.

Container builds are where this silently breaks. Pipelines that run composer install at image-build time and artisan commands at boot routinely skip post-autoload-dump, which is where filament:upgrade (and therefore filament:assets) normally hangs. Verify empirically against a staging artifact — hit the editor and confirm the shipped JS matches the new version — rather than inferring from the Dockerfile.

previewData() is a data-egress channel with a friendly name. It's JSON, it's signed, it goes to the browser, and anyone who can open the editor can read it. Engineers reflexively pass model attributes here. Write the allow-list rule down in your adoption notes; a README footnote will not survive contact with a sprint.

The $data === null idiom is a silent break. Once a field supplies previewData(), bricks using null as their "am I in the editor?" sentinel produce wrong output with no error. Exposure is opt-in per field, so it's contained — but audit before enabling, not after a support ticket.

view:clear once after upgrading to 3.6.0. #74 moved @masonStyles / @masonEntryStyles from compile-time to render-time resolution. Old compiled views keep the old behaviour until cleared. Low severity, easy to miss, presents as "styles didn't update after deploy."

Renaming bricks orphans content. There is no schema contract protecting you. A renamed identifier leaves stored JSON that no longer resolves. Plan a retiredBricks fallback or a data migration; don't discover this on a live page.

Your deploy ordering matters more than your code review. Composer bump → filament:assets → view:clear, in that order. Reversing it defeats the point.

Pin the version and read the changelog. MIT, community-maintained, single maintainer, no SLA and no guaranteed backports to older minors. Pin to a minor (^3.6), read release notes before bumping, and be prepared to carry a patch yourself if a security fix lands on a branch you can't move to. The new security page is citable in a threat model — use it — but it also makes clear that escaping, sanitization, and raw-HTML policy across your brick library are your code, not the vendor's.

Extension points worth knowing about. The two that matter operationally are the field-level previewData() and the preview/entry style directives. Bricks are your extension point for everything else — if you find yourself wanting to hook into the editor's JS, you're probably building the wrong thing and should reassess whether the content model fits.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console