-
Notifications
You must be signed in to change notification settings - Fork 0
Usage
Pages in the panel nav → a page row → its settings → Edit page content → the builder, full screen in a new tab.
Settings and content are deliberately separate screens. Slug and SEO are things you set once; content is what you come back to.
- Title. Internal name, and the fallback for the meta title.
-
A tab per locale, each holding:
- Slug. Leave empty to generate one from the title.
- Meta title and meta description.
- Social share image. 1200 by 630 is the safe size.
- Canonical URL. Empty means the page's own URL.
Header buttons: Edit page content, View live (published pages only), Publish, Delete.
Full screen, outside the panel chrome.
Left rail switches between the section list and the inspector.
Sidebar shows one of two things:
- The section list: every section, labelled by its own heading rather than "Block 4". Hover a row for move up, move down and settings. Add section at the bottom opens the picker, grouped by category.
- The inspector for the selected section: its fields, with move, duplicate, hide, delete in the header and a back chevron to the list.
Middle is the live preview. It renders the real page through the public layout and the public stylesheet, so it is not an approximation. Click a section in the preview to select it.
Toolbar:
- Back arrow to the Pages list
- Status badge: Draft, Published, or Published with unpublished changes
- Desktop, tablet and mobile widths, at fixed sizes rather than whatever the pane happens to be
- Locale switcher
- Open the preview in a new tab, as a signed link that expires
- Publish
Editing writes the draft, always. The live page reads a separate column and cannot change until you press Publish. An unpublished page 404s on the public site, so a half-finished page can't leak.
Hiding a section keeps it in the editor and removes it from the public page. It's the reversible alternative to deleting.
A page can be wrapped in whichever shell suits it: a navbar and footer for marketing, a sidebar for documentation. Register them in the panel provider and the client picks one from a Layout dropdown in page settings. See Layouts.
One PHP class and one Blade view. Nothing inside the plugin changes.
namespace App\Blocks;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Safi\Atelier\Blocks\BaseBlock;
use Safi\Atelier\Media;
class StatsBlock extends BaseBlock
{
public static function type(): string
{
return 'stats'; // registry key, and the view name
}
public static function icon(): string
{
return 'heroicon-o-chart-bar';
}
public static function category(): string
{
return 'Content'; // groups it in the picker
}
/** Fields stored per locale. */
public static function translatable(): array
{
return ['heading'];
}
/** Starting values when the block is added to a page. */
public static function defaults(): array
{
return ['heading' => ['en' => 'By the numbers']];
}
public function schema(): array
{
return [
TextInput::make('heading')->live(debounce: 400),
Textarea::make('body')->rows(3)->live(debounce: 400),
Media::upload('image', 'Image')->live(),
];
}
}Then resources/views/blocks/stats.blade.php in your app:
<section data-atelier-block="{{ $id }}" class="px-6 py-16">
<div class="mx-auto max-w-3xl">
@if ($heading = $attributes['heading'] ?? null)
<h2 class="text-3xl font-semibold">{{ $heading }}</h2>
@endif
</div>
</section>And register it:
AtelierPlugin::make()->blocks([
...DefaultBlocks::all(),
\App\Blocks\StatsBlock::class,
])-
->live(debounce: 400)is what makes the preview update as you type. A field without it only refreshes when focus leaves the field. This is the single most common "why isn't the preview updating". -
translatable()fields are stored as{"en": "...", "ar": "..."}. Everything else is shared across locales. Repeaters can be translatable too; then the whole list is per locale. -
Use
Media::upload()in the schema andMedia::url()in the view. Never callStorage::url()yourself.FileUploadstate is not reliably a string; it's an array keyed by uuid while editing and[]when empty.Media::url()is where that's handled. -
Put
data-atelier-block="{{ $id }}"on the outer element. That's what makes clicking a section in the preview select it in the sidebar.
| Variable | What it is |
|---|---|
$attributes |
The block's fields, already collapsed to the current locale |
$id |
Stable block id |
$locale |
Current locale code |
$editing |
True in the preview, false on the public page |
$children |
Rendered child blocks, for nesting |
Use $editing to show something in the editor that shouldn't ship, like an empty-state placeholder.
| Block | Notes |
|---|---|
| Hero | Optional background image, left or centre |
| Features | Repeater, 2 to 4 columns, Heroicon per item |
| Rich text | Filament's rich editor, stored as HTML |
| Image | Container, wide, or full bleed |
| Gallery | Repeater of images, 2 to 4 columns |
| Logo wall | Repeater with optional links |
| Testimonials | Repeater with quote, name, role, photo |
| FAQ | Repeater. Renders as <details>, so it works with JS off |
| Call to action | Heading, text, button |
Only if you're changing Atelier, not when consuming it:
npm run build # rebuild the editor's own stylesheet
cd example && php artisan filament:assetsThe editor's utilities ship compiled in resources/dist/atelier.css and are registered through FilamentAsset, so consumers never run a build.
Building
Running a site
Reference