-
Notifications
You must be signed in to change notification settings - Fork 0
How it works
The short version: Atelier copies Gutenberg's data model and skips its storage model.
[
{
"id": "b_8f3a",
"type": "hero",
"attributes": {
"heading": { "en": "Welcome", "ar": "أهلا بك" },
"align": "center"
},
"children": []
}
]Stored as JSON, rendered by Blade at request time. In Gutenberg terms every block is a dynamic block.
Gutenberg stores rendered HTML as the source of truth and pays for it with three subsystems: attribute sourcing (scraping data back out of markup), block validation and "this block was modified externally", and runtime deprecation chains. None of those exist here, because none of them are needed when the tree is the truth. Changing a block's attribute shape is an ordinary data migration.
atelier_pages has draft_content and published_content as separate JSON columns.
Editing writes the draft. Publishing copies draft to published. The public route reads published and nothing else. That's what makes it impossible to publish a half-typed sentence by accident, and it's why an unpublished page 404s rather than rendering.
atelier_page_slugs, with a unique index on (locale, slug). A JSON map can't carry a unique index, and two pages sharing a slug in one locale is a real bug rather than a hypothetical.
A translatable attribute holds a map keyed by locale:
"heading": { "en": "Welcome", "ar": "أهلا بك" }One tree, one section order, translated text. Arabic is mirrored with dir="rtl" and CSS logical properties, not by reordering blocks.
The accepted cost: Arabic cannot have a different section order from English. For a marketing site that's the right trade, and it stops the two languages drifting into different pages. A missing translation falls back to the default locale, so a half-translated page reads as untranslated rather than broken.
public static function type(): string; // registry key
public static function label(): string; // shown in the picker
public static function icon(): string;
public static function category(): string; // groups it in the picker
public function schema(): array; // Filament components = the settings form
public static function supports(): array; // shared features it opts into
public static function translatable(): array; // which fields are per-locale
public static function defaults(): array; // starting valuesschema() returning a plain Filament schema is the biggest saving in the project. The entire control system, validation and field rendering come free, and any Filament field works: text, select, rich editor, file upload, repeater.
Blocks register at boot into a BlockRegistry. Adding one never means editing a file inside the plugin.
atelier_page_revisions holds a snapshot of the tree per publish, with who published it,
pruned to a configurable count. atelier_page_redirects maps an old slug to a page,
not to another slug, which is what makes redirect chains impossible: a page renamed twice
leaves two rows both resolving to wherever it lives now.
None of these were in the first migration. New tables always ship as new migration files,
never as edits to one that already ran on somebody's database, so upgrading is
vendor:publish plus migrate and never a conflict.
The blocks are the page. The shell around them is a per-page choice from a registry of layouts, resolved by one method that both the public controller and the preview call, so the preview cannot drift onto a different shell. A layout key nobody registered falls back to the configured default rather than throwing, because a page keeps its key after a developer deletes the layout.
Colour, type and spacing are emitted as CSS custom properties into the head of both the
public page and the preview, from the same layout. A block attribute stores
{"token": "color.primary"} and the renderer resolves it to var(--atelier-color-primary)
before the view runs.
Storing the reference rather than the value is what makes a palette change a config edit instead of a data migration, and it is the other half of why the preview cannot lie: both sides read the same variables from the same place.
The middle pane is an iframe pointing at a signed, noindex preview route. That route renders the draft tree through the public layout and the public stylesheet. Same Blade views, same CSS, different data source.
This is the one rule the whole feature rests on. A second rendering path for the editor would make the preview an approximation, and an approximation is what everyone already has, in another browser tab.
Refresh works by fetching the preview and swapping the contents of [data-atelier-canvas] inside the iframe. The document never reloads, so scroll position survives for free and the stylesheet never re-fetches, so there's no flash. A twelve-section render measures 16ms, which is why full-page rendering is fine and per-section rendering isn't needed yet.
Three bugs in this project came from the same mistake: reading Livewire's raw state instead of going through Filament's flow.
Filament fields keep an internal representation that isn't what should be stored. The rich editor holds a TipTap JSON document. FileUpload holds an array keyed by uuid, stores [] when empty, and only moves the temporary file onto the disk inside a beforeStateDehydrated hook.
So the editor fills through the form and reads the dehydrated state, running the hooks and casts in both directions. If you extend this and a field's value arrives looking wrong, that's the first place to look.
Blocks are styled with Tailwind, and Tailwind scans source files. It cannot see a vendor directory it wasn't told about, which is why the install needs an @source line, and why skipping it silently produces unstyled blocks.
The editor's own utilities are a different problem: Filament's panel CSS only contains classes Filament itself uses. So the package ships its own compiled stylesheet in resources/dist/, registered through FilamentAsset, and a consumer never runs a build.
Z3d0X/filament-fabricator is the obvious foundation, and this project deliberately doesn't use it. Not because it's unmaintained or lacks Filament 5 support; it has both. Because Atelier's spec already replaces four of the five things Fabricator provides: the edit screen, the single content column, the slug on the record, and the block abstraction. What's left is a route and a slug lookup, and taking a dependency that shapes the page model in order to then override the page model is the worst of both.
The full reasoning, including the correction of two false claims made against Fabricator, is in Docs/prd.md in the repository.
Building
Running a site
Reference