Repository navigation
Releases: Abdulkader-Safi/filament-atelier
Release list
v1.0.0
The version number, and the two editor changes that were worth waiting for.
composer update safi/filament-atelier
php artisan filament:assets # re-publishes the panel stylesheetNo migration, no config change. If you wrote your own layout, delete the editor script from it: the preview injects it now, and a copy in the layout only runs twice.
What 1.0.0 means
The public API is named in the README and will not move inside 1.x: Block, PageType, AtelierPlugin's registration methods, the documented methods on Page, Media, Tokens, Url, MenuSource, the two head partials, the data-atelier-canvas contract, every config key, and the table shapes. The registries, the Filament resources, the renderer and the controllers are internal and may change in a minor release.
It does not mean feature complete. "Not built yet" in the README is still accurate and still honest: no raw HTML block, no contact form block, no revisions UI, and the menu manager still ships behind a flag.
Added
A Pages panel in the editor. A third button in the icon rail listing every page grouped by type, with a search box and the current one marked. Clicking one opens it in the builder, with the address bar following, so refresh and the back button behave. Editing a header on one page and a footer on another stops being ten clicks each way.
Links in the preview open the page they point at. Clicking a nav item in the preview used to navigate the iframe to that page's public URL, leaving the canvas showing one page while the section list described another, silently. Now it switches the builder to that page. A link Atelier does not own opens in a new tab and the builder stays where it was.
Drag to reorder sections. Same gesture and same library as the menu manager, with a handle on the section icon so a click still selects. Moving a section from the bottom of a twelve-section page to the top is one drag rather than eleven clicks and eleven preview refreshes. The up and down buttons stay for the keyboard.
Safi\Atelier\PageResolver. One implementation of "which page is this URL", including the rule that the first segment is a locale only when it names one. The public controller and the editor both call it.
Changed
The preview's editor script is injected by the controller, not carried by the layout. It sat inside the shipped layout behind an @if ($preview), so every layout needed a copy and a hand-written one silently lost section-clicking. A layout now carries nothing for the editor beyond data-atelier-canvas.
Forms inside the preview no longer submit. A form in a block used to post for real from the editor, against a page that may not be published yet.
Fixed
The home page had two URLs and the canonical pointed at the wrong one. The page whose slug is home is served at /, but Page::url() built /home, and that is what the canonical tag, the hreflang alternates, the sitemap entry and every menu item built from the home page carried. Duplicate content with the canonical naming the copy, on a package that sells itself on SEO. url() now returns the root of the locale and /home 301s to it.
A URL typed in the panel could carry a javascript: scheme into an href. Blade escaping stops the value breaking out of the attribute and does nothing about the scheme, so a link pasted into a hero, a call to action, a logo wall or a menu item was stored XSS against every visitor. Url::safe() allows http, https, mailto, tel and sms and returns # otherwise. Call it in your own blocks wherever a client-supplied URL reaches an href.
Every link in the preview opened in a new tab when APP_URL did not match the address being browsed. The ordinary local setup, APP_URL naming localhost while somebody browses 127.0.0.1, treated the whole site as external.
Documentation
The wiki is current, including a new Page types guide. example/ is a complete demo site with DEMO.md as its tour.
Full changelog: v0.5.0...v1.0.0
v0.5.0: Page types
Page types: a service, a product or a case study becomes its own entry in the sidebar, with its own fields and its own starter sections, on the same table and the same editor as every other page. Plus the block that lists them.
composer update safi/filament-atelier
php artisan vendor:publish --tag=filament-atelier-migrations
php artisan migrate
npm run build # only if your app compiles the package's block viewsThe migration adds two columns to atelier_pages and touches no existing data: every page you have becomes an ordinary page, which is what it already was. Registering no page types leaves the panel exactly as it was.
Added
Page types. One plain class per type, registered from the panel provider:
AtelierPlugin::make()->pageTypes([ServiceType::class]);A type declares its custom properties as a plain Filament schema, the block tree a new page of that kind opens with, the blocks its picker offers, a slug prefix, a schema.org type, the card view a listing renders it through, and optionally a view served at the prefix root. type() and label() are the only two required, so a type with no fields of its own is six lines.
Each registered type gets its own sidebar entry, its own list and its own URL (/admin/services), all served by the one PageResource through Filament's resource configurations. Nothing needs a resource class per type. Pages itself now lists only untyped pages, plus anything carrying a type nobody registered, so deleting a type class never hides a live page from the panel.
Two columns on atelier_pages: type, defaulting to page and indexed, and data for whatever the type declared. Untranslated values live at data.{key}, translated ones at data.{locale}.{key}, the same shape the seo column already uses. $page->data('excerpt', $locale) reads either, falling back to the first configured locale.
A Collection block. Lists pages of one type and renders each through that type's own card view, so a services grid needs no code. Everything published, hand-picked in an order you drag, or the pages sitting under the current one by slug path.
$page in every block view, so a block can know which page it is on and read that page's own fields.
A generated index per type at its prefix, listed in the sitemap once the type has something published. A page you build at the same slug wins, and so does a redirect left behind by one that moved.
Changed
The hero button and the call-to-action panel are painted from the design tokens rather than a hardcoded neutral. A site that sets color.primary in config/atelier.php now gets its own colour on both without overriding a view. If you were relying on the black panel, set color.primary to #171717 and nothing moves.
The structured-data select is labelled "Type for search engines" rather than "Page type". The stored value and the options are unchanged.
PageResource::form() is four methods rather than one, which only matters if you extend the resource.
Fixed
Page::children() was written in SQL that only some databases accept. Double quotes around a string literal work on SQLite and on MySQL without ANSI_QUOTES, and are a column name everywhere else, so a CollectionPage would have thrown a syntax error on Postgres rather than rendering.
Removed
preview.debounce in config/atelier.php. Nothing ever read it. The real debounce is ->live(debounce: 400) on each field in a block's schema.
The example app
example/ is now Sparkle Clean, a small cleaning company: five ordinary pages, three services with three pricing tiers each, three products with variations, request forms landing in a Requests inbox, and a dashboard counting what sold. Every block it uses is defined in example/app/Blocks rather than pulled from the package, so the folder reads on its own. example/DEMO.md is the tour. It is export-ignored, so composer require still pulls the package and nothing else.
Not 1.0.0 yet: the gates are listed in Docs/tasks/14-v1-release.md.
Full changelog: v0.3.6...v0.5.0
v0.3.6
Two editor fixes. No migration, no config change. composer update safi/filament-atelier and php artisan filament:assets, which re-publishes the panel
stylesheet.
Fixed
-
Deleting a row from a repeater field did not stick. The row disappeared, the preview
kept it, and it was back on the next load. The editor wrote form changes into the block
tree fromupdatedData(), a Livewire hook that fires on a field update from the browser and
nothing else. A Filament action writes component state on the server instead, so deleting a
repeater row, adding one or reordering them changed the form and never reached the tree.The sync now also runs on
dehydrate(), at the end of every request, after any action has
run. That covers every action a block schema brings with it rather than one hook per field,
and it only writes when the tree actually moved, so a click or a locale switch does not fire
a pointless save. If a row you deleted before this release is still on the page, delete it
again and it will stay gone. -
The Add section picker still ran off the bottom of the screen. 0.3.4 capped its height
with a Tailwind class and 0.3.5 shipped the stylesheet that class needed, but a panel that
had not re-published its assets was still uncapped, and the cap alone did not stop a tall
list from stretching the whole page. The cap is now an inline style, so it cannot be lost to
a stale stylesheet, and the panel clips rather than growing past the viewport.
Changed
- The section picker is capped at two thirds of the screen rather than 45%, and the
section list behind it stays visible.
v0.3.5
Ships the stylesheet 0.3.4 should have shipped. composer update safi/filament-atelier is the
whole upgrade.
Fixed
-
The Add section picker still would not scroll on 0.3.4. The fix was real but half of it
never left this repository. Atelier compiles its panel CSS ahead of time into the package so
a client installs with no build step, Tailwind only emits the classes it finds in the views
at build time, and 0.3.4 changed a view without rebuilding. Somax-h-[45vh]was in the
markup and nowhere in the stylesheet, leaving the list uncapped and unscrollable exactly as
before. Searching worked throughout because that part is Alpine, not CSS.Same rebuild also brings in the search input's focus ring and placeholder colour.
v0.3.4
The section picker, on a site with a lot of block types. No migration, no config change.
composer update safi/filament-atelier is the whole upgrade.
Fixed
- The Add section picker ran off the bottom of the panel with no way to scroll it. The
list had no height of its own, so past roughly twenty block types everything below the fold
was unreachable: the editor is a fixed-height layout that clips rather than scrolls, and
the panel's own scroll belongs to the section list above. The picker now caps at 45vh and
scrolls inside itself.
Added
- A search box on the picker. It opens focused, so adding a section is now type a word,
click the block. Typing filters by label and hides a category once nothing in it matches,
with a line saying so when nothing matches at all. Escape clears the box, then closes the
picker.
v0.3.3
A menu manager, behind an experimental flag that is off by default, and a fix for a crash
that only showed up next to Shield.
This release adds a table, used only if you turn the menu manager on. Publishing and
migrating is still the safe order:
composer update safi/filament-atelier
php artisan vendor:publish --tag=filament-atelier-migrations
php artisan migrateLeaving the flag off changes nothing about an existing site. No new page, no sidebar entry,
no route.
Added
-
A menu manager, experimental and off by default. Named navigation menus per location,
edited from the panel and rendered on the public site withMenu::treeFor(). Drag and drop
reorders, nests a top-level item under another and promotes one back out. Labels and URLs
are per locale, an item can point at an Atelier page or anything else you register as a
MenuSource, and an item can be hidden without deleting it.Locations come from config, so a theme decides that it has a header and a footer rather
than a client inventing menu names. One row per location, holding the whole tree as JSON.
src/Models/Menu.phpsays why that beats a nested-set table here.Turn it on in
config/atelier.php:'experimental' => [ 'menus' => true, ],
Or per panel, which overrides config rather than adding to it:
AtelierPlugin::make()->experimental(['menus' => true])
It is experimental because it is a real page in a client's sidebar the moment it is on, and
it has not been through a client project yet. Off means the page and its route are never
registered, not just hidden. The Menus guide
covers the rest.
Fixed
- Opening a Shield role wrecked the whole screen if Atelier was installed. Shield builds
its role form by instantiating every registered page class and asking each one for its
title.PageEditoronly receives its record inmount(), so an instance Shield made by
hand hit an uninitialised typed property and threw, taking the role edit screen with it.
getTitle()now answers without a record.
v0.3.2
A bug fix. No migration, no config change, no new table. composer update safi/filament-atelier is the whole upgrade.
Fixed
-
The preview iframe kept showing the old locale after switching languages. Its refresh
fetch read a preview URL cached in the Alpine component at page load rather than the
iframe's currentsrc. Livewire moves the iframe'ssrcto the new locale on every
render; the cached copy never followed, so each refresh re-fetched the old locale and
painted it back over the iframe. The fetch now reads the iframe's ownsrc. -
The same locale switch could also fail the preview request outright, independent of the
fix above.setLocale()calledpersist(), which writes the block tree, unchanged,
there is nothing to save, and dispatchesatelier-refresh, firing a second fetch at the
exact preview URL the iframe'ssrcchange was already navigating to. Both requests carry
the same session cookie and race each other, and the loser can fail.setLocale()no
longer callspersist(); the iframe's own navigation is enough.
v0.3.1
A bug fix. No migration, no config change, no new table. composer update safi/filament-atelier is the whole upgrade.
Fixed
-
Removing an image in the page editor did not save the removal. The field cleared, but
the block tree kept the old path, so the preview still showed the image and publishing put
it back on the live page. Filament removes a file through a renderless method that writes
component state directly, so the editor'supdatedData()hook never ran and the cleared
value never reached the tree. The upload field now hooks the removal itself.If a page already has an image you thought you deleted, open the section, remove it again
and it will stick this time. -
Removed and replaced images were never deleted from the disk. Filament only deletes a
stored file whendeleteUploadedFileUsingis set, and it was not, so every image you
removed or replaced stayed on the disk with nothing referencing it. New removals now delete
the file. Files orphaned before this release are still there and need clearing by hand.
v0.3.0
Structured data. Every page now emits a JSON-LD graph describing the site, the page and what
the page is about, from three sources: a settings screen, a page type select, and blocks that
describe themselves.
It also fixes a routing bug that has been there since the first release and is worth reading
even if none of the rest interests you.
This release adds a table and a column. After updating:
composer update safi/filament-atelier
php artisan vendor:publish --tag=filament-atelier-migrations
php artisan migrateNothing else is required. A site that fills in none of the new screens still emits a valid
graph, because everything derivable is derived.
Fixed
-
Atelier's catch-all was shadowing the host app's own routes. The package registered its
routes as its provider booted, which happens before Laravel loadsroutes/web.php, and
Laravel matches in registration order. So an app's own/blog/{slug}lost to Atelier's
/{locale}/{slug?}and 404'd, which is the opposite of what this package documents and
promises. Routes now register from abooted()callback, after every provider, so the
catch-all really is last. Route caching still works.If you worked around this by moving routes into a service provider, renaming a path, or
putting your blog on a third segment, you can undo it after upgrading.
Added
-
A JSON-LD graph on every public page. One
<script type="application/ld+json">holding
theOrganization(or the LocalBusiness subtype you pick), theWebSite, thisWebPage
with its language and dates, aBreadcrumbListderived from nested slugs,ImageObject
nodes for the logo and share image, and whatever the page is about.Nodes reference each other by
@idrather than repeating the organisation on each one.
Nothing is stored: the graph is built at render from data you already filled in, so there
is no second copy to keep in step and no cache to clear. Empty values never appear, so a
page with nothing filled in is still valid rather than a node full of nulls. The Arabic
page describes the same organisation in Arabic, pointing at the same@id. Previews emit
nothing, beingnoindexanyway.The encoding is a security boundary rather than formatting: a client typing
</script>
into a meta title cannot close the block, and Arabic stays readable rather than becoming
\uXXXXescapes. -
A Site details screen, under a Settings group in the panel. The organisation behind the
site: name and description (translatable), legal name, logo, type, social profiles, and for
a business with premises its address, geo, price range, areas served, opening hours and
contact points, plus founding date, employee count, VAT and tax numbers.It is a screen rather than config on purpose. Tokens, locales and layouts are a developer's
decisions and belong in a file. An address is client-owned data that changes without a
deploy, and the person who knows it does not have a text editor open.Hours are one row per set, listing the days that share them, which is both what schema.org
wants and how a person thinks about them. A contact point says who answers and in which
language, which a bare telephone number does not. -
A page type select, on the page settings screen under Structured data: standard page,
about, contact, listing, article, service, product, event, person or job vacancy. Choosing
one reveals the few fields it needs and nothing else, and none of them repeat a field the
page already has, since the name and description come from the meta fields, the image from
the share image and the dates from publishing.Page-shaped types refine the
WebPagenode itself, because an About page is a web page.
Thing-shaped types become their own node linked throughmainEntity, because a page about
a product is not a product. The type is page-level, not per locale: a page that is a
Service in English is a Service in Arabic.Defaults do the work where they can. An Article with no author credits the organisation. An
Event defaults to going ahead and in person, so cancelling is one select and a cancelled
event stops advertising itself. A vacancy dates itself from the publish date and locates
itself from the site address, and marking it remote setsjobLocationType, without which a
remote role is filtered out of remote searches. A listing page emits anItemListof the
pages directly under it, derived from the slug path, so a services index stays right when a
service is added. -
FAQ schema, two ways. The FAQ block turns its questions into
FAQPageautomatically,
with nothing typed twice: two FAQ blocks on one page merge into a single node, a question
with no answer is dropped, and a hidden section contributes nothing.Questions can also be typed under Structured data, per locale, on a page built from
anything at all. That is not a fallback: on a site whose blocks you wrote yourself, a custom
FAQ section has no schema unless somebody remembered to add it, and nobody should edit a PHP
class to get an FAQ into the head. Typed entries win over derived ones. Note that Google
expects FAQ data to correspond to something a visitor can see, so this is for content on the
page in another form, not for questions that appear nowhere.Breadcrumbs take a mode in the same place: from the slug path (the default), typed by hand
for a page whose slug is not its hierarchy, or off. -
Blocks can describe themselves.
structuredData($attributes, $locale, $url)on a block
returns nodes for the page's graph, and a node whose@idis already there merges into it.
The attributes arrive collapsed to the locale with tokens resolved, exactly as the view
receives them, so the schema cannot describe something different from what rendered. Adding
a block that contributes schema needs no change inside the plugin. -
Your own routes can share the graph. A blog post or a services record lives on your
route, in your view, and Atelier never sees it, but it should not have to reinvent the
organisation:@include('atelier::partials.schema', ['nodes' => [[ '@type' => 'Article', '@id' => url()->current().'#article', 'headline' => $post->title, 'publisher' => ['@id' => \Safi\Atelier\Schema\StructuredData::siteId('organization')], ]]])
It emits the
OrganizationandWebSitenodes plus whatever you pass, with the same
pruning, merging and escaping. The publisher is then the same node the rest of the site
points at rather than a copy that drifts the first time a phone number changes. Pair it with
->sitemap([...])and a blog is in the sitemap and in the graph without Atelier knowing it
exists.
Deliberately not included
ReviewandAggregateRatingfrom testimonials. Google ignores reviews a business
publishes about itself, and the block has no rating field to aggregate.HowTo. Its rich results were dropped in September 2023, so it is markup for nobody.WebSiteSearchAction. The sitelinks search box was deprecated in November 2024.
Full changelog: v0.2.0...v0.3.0
v0.2.0
Multiple layouts, picked per page.
A minor rather than a patch because it adds a real capability and a new registration method, not because anything breaks. No migration, and nothing to change: every existing page keeps rendering through config('atelier.layout') exactly as before.
Multiple layouts
A site is rarely one shell. Marketing pages want a navbar and a footer, documentation wants a sidebar, a landing page often wants neither. The blocks are the same either way, so the shell is now a per-page choice rather than a different set of blocks.
Register the layouts you have written where you register blocks:
AtelierPlugin::make()
->blocks(DefaultBlocks::all())
->layouts([
'site' => ['label' => 'Navbar and footer', 'view' => 'layouts.site'],
'docs' => ['label' => 'Sidebar', 'view' => 'layouts.docs'],
'bare' => 'layouts.bare',
]);The short form is just a view name and the label comes from the key. A Layout dropdown appears on the page settings screen, and hides itself entirely when no layouts are registered, because a question with one answer is not a question. Leaving it empty uses the site-wide atelier.layout.
A working example ships in example/resources/views/layouts/docs.blade.php, and the Layouts guide covers writing one.
Three decisions behind it
A layout is a map, not a class. Blocks earn a class because they carry a schema, an icon, a category and translatable keys. A layout is a key, a label and a view, and a class holding three strings is ceremony.
The choice is page-level, not per locale. A layout is structure, and both locales share one structure by design, the same reason Arabic cannot have a different section order from English.
An unknown key falls back rather than throwing. Delete a layout from your panel provider and pages still naming it render on the site-wide default. A page keeps its key after the code that defined it is gone, and a 500 on every public page is a bad way to find that out.
Fixed
The preview never passed $page to the layout, so a custom layout reading it worked on the live site and 500'd in the preview. That is exactly the failure the shared render path exists to prevent, and it was found by the test asserting a preview renders through the same layout as the public page.
Known issues
- No JSON-LD yet. Meta, canonical, hreflang, Open Graph, a sitemap and per-page noindex are in; structured data is not.
Full changelog: v0.1.6...v0.2.0