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

Filament Atelier Laravel Package

safi/filament-atelier

A visual page builder plugin for Filament. Developers define section blocks in PHP with a Blade view and a settings schema; clients assemble pages in a full-screen editor with a live, responsive iframe preview. Pages are stored as JSON and rendered server-side by Blade. Multi-language.

View on GitHub
Deep Wiki
Context7

Getting Started

Install in the order the wiki gives, not the order your muscle memory gives.

composer require safi/filament-atelier
php artisan vendor:publish --tag=filament-atelier-migrations
php artisan migrate && php artisan storage:link

Then register the plugin in your panel provider. Start with the shipped blocks only — resist writing a custom block until a page has rendered end-to-end once:

use Safi\Atelier\AtelierPlugin;
use Safi\Atelier\Blocks\DefaultBlocks;

->plugin(
    AtelierPlugin::make()->blocks(DefaultBlocks::all())
)

The one step everyone skips: point Tailwind at the package's views. On Tailwind 4, in your app.css:

@source '../../vendor/safi/filament-atelier/resources/views/**/*.blade.php';

Without it the editor chrome and the package's own partials compile to unstyled markup with no error in the console. This is the first thing to check when the editor "looks broken."

Where to look first, in this order: the Installation wiki page (it lists the steps that fail silently), config/atelier.php after publishing it, then the Troubleshooting page before you file anything. Docs/ in the repo is the PRD and design history for people working on Atelier — it is export-ignored and it is not user documentation.

First use case: register a layout with your tokens, create one page, add a hero and a features block, publish it, and hit the public URL and the preview iframe side by side. If those two don't match, stop and fix it there — everything downstream depends on the preview being the real render.

One honesty note before you plan a sprint around this: the method names in the block examples below follow the README's description (key/label/icon/category/schema/supports), not verified source. Confirm the exact signatures against the Blocks wiki page before you generate twenty of them.

Implementation Patterns

A block is your code, in your app, with your view. Keep them out of vendor/ and out of a giant single file:

namespace App\Atelier\Blocks;

use Filament\Forms\Components\{TextInput, Repeater, RichEditor};
use Safi\Atelier\Blocks\Block; // confirm namespace against the wiki

class PricingTable extends Block
{
    public static function key(): string      { return 'pricing-table'; }
    public static function label(): string    { return 'Pricing table'; }
    public static function icon(): string     { return 'heroicon-o-currency-dollar'; }
    public static function category(): string { return 'commerce'; }

    public static function schema(): array
    {
        return [
            TextInput::make('heading')->required(),
            Repeater::make('plans')->schema([/* … */])->minItems(1),
        ];
    }

    public static function supports(): array
    {
        return ['background', 'spacing'];
    }
}

The class's Filament schema is the client's settings pane — so treat every field as a design decision, not just a storage decision. Defaults matter more than validation here: a client who adds a block and sees a sane, on-brand result will not call you.

Register blocks from one place, not per panel. A config/atelier.php array or a small BlockRegistry means the block list and the sitemap sources live together and your panel provider stays three lines long:

->plugin(
    AtelierPlugin::make()
        ->blocks(array_merge(DefaultBlocks::all(), [
            \App\Atelier\Blocks\PricingTable::class,
            \App\Atelier\Blocks\TeamGrid::class,
        ]))
        ->sitemap([
            fn () => Post::published()->get()->map(fn (Post $post) => [
                'loc' => route('blog.show', $post),
                'lastmod' => $post->updated_at,
            ]),
        ])
)

Sitemap sources are closures resolved at request time, so queries are free — but they run on every sitemap hit, so cap the result set or eager-load rather than shipping an unindexed get() over 50k rows.

Layouts and design tokens are one contract. The preview iframe loads the public layout and public stylesheet, so colours, fonts, spacing and max-width must be CSS custom properties defined once in the layout. Your blocks then read var(--atelier-space-lg) and never a hard-coded py-16. If a block hard-codes a value, the preview and the site still match — you've just made the token layer a lie. Audit for this in review.

Non-Atelier routes reuse the same JSON-LD graph. Don't reinvent the org node in a blog controller:

@include('atelier::partials.schema', [
    'nodes' => [[
        '@type' => 'Article',
        '@id' => url()->current().'#article',
        'headline' => $post->title,
        'datePublished' => $post->published_at?->toAtomString(),
        'publisher' => ['@id' => \Safi\Atelier\Schema\StructuredData::siteId('organization')],
    ]],
])

Fill Settings → Site details once, at project start, before the client notices. Every page's schema graph points at that single organisation @id.

Publishing flow to teach the client on day one: draft and published are separate columns, so editing never touches the live page; preview links are signed and expiring, so you can email one to a stakeholder without opening the panel; each publish writes a revision snapshot. The restore exists (restoreRevision()) but there's no screen for it — if revisions matter to the contract, wrap it in a small Filament action or an artisan command yourself, because a feature with no UI is a feature the client will assume doesn't exist.

Multi-language is per page, not per site. One page can carry any number of locales from config/atelier.php, each with its own slug, hreflang, dir and SEO. Decide the slugging strategy (translated slugs vs. prefixed identical slugs) before the first page ships — changing it later means moving URLs, and the 301 mechanism only covers slug renames through the editor.

Gotchas and Tips

Re-publish migrations after every composer update. New tables ship as new migration files, and vendor:publish skips files you already have — so re-running it is safe and skipping it produces a "no such table" error the first time the new feature is used, usually in front of a client. Make it part of the deploy script, and read CHANGELOG.md for the releases that require it.

Never delete a block class that pages reference. Blocks are code, content is a JSON tree of typed keys — delete or rename key() and existing pages hold orphaned nodes. You'll typically find out on a published page, not in the editor. Keep a deprecated block around rendering its old markup, add the replacement, migrate data with a one-off script, and only then remove it.

The preview inherits every constraint of your layout. If the layout touches auth(), a cart, or a locale-aware middleware, the iframe will break or show the wrong shell. Layouts must be dumb: tokens, @yield/$slot, navbar and footer, nothing else.

"No HTML in the database" is about structure, not rich text. A rich-text or raw-ish block still outputs HTML — render deliberately and sanitize on the way out if the editor allows it. Likewise, if you build the missing contact-form block, it is presentational per the README: wire your own POST route, validation, honeypot and rate limiting. Budget half a day for that, not zero.

Reordering is up/down buttons and new sections land at the end. No drag, no insert-anywhere. It's fine in practice, but tell the client before they try, and design long pages so an out-of-order section is a cosmetic problem rather than a structural one.

Perf is a posture, not a feature yet. No page cache, no per-block CSS/JS. So: don't let each block pull its own CDN library (one GSAP, one carousel lib, loaded from the layout), and keep the block count on a page sane. The sitemap is generated per request with no cache — that's a feature at small scale and a query-count tax if your sources multiply.

Structured data: pick derived or typed, not both. An FAQ block emits FAQPage automatically; typed FAQ entries under page settings win over derived ones. If you type them and leave the block in, you're one duplicate node away from a Search Console warning. Same discipline for breadcrumbs on nested slugs.

Pin the version and stay close to the release notes. MVP, 34 stars, zero dependents, one maintainer. That's fine for a client site — the code is MIT and the architecture is deliberately boring — but it means you are the QA team. Practically: pin in composer.json, read CHANGELOG.md before updating, keep your block set small and your views in your own repo so a fork or a hand-patch is a morning, not a rewrite.

Debug in this order when something renders wrongly: (1) Tailwind @source path, (2) storage:link and the image/logo paths, (3) plugin registration actually in the panel that's serving the request, (4) layout chosen on the page settings vs. the layout you assumed, (5) whether the page is published and not noindex-ed for that locale. Four of those five fail silently, which is why the wiki keeps a troubleshooting page.

Test the render, not the editor. A tiny Pest test that seeds one page containing one of every registered block and asserts a 200 plus a couple of assertSees catches the orphaned-key and broken-view class of bug far earlier than clicking through Livewire does.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings