Skip to content

Releases: Abdulkader-Safi/filament-atelier

v1.0.0

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 13 Sep 10:02

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 stylesheet

No 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

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 13 Sep 08:23
61862c2

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 views

The 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

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 03 Sep 12:41

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 from updatedData(), 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

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 01 Sep 14:56

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. So max-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

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 01 Sep 14:48

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

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 01 Sep 14:36

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 migrate

Leaving 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 with Menu::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.php says 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. PageEditor only receives its record in mount(), 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

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 25 Aug 06:48

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 current src. Livewire moves the iframe's src to 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 own src.

  • The same locale switch could also fail the preview request outright, independent of the
    fix above.
    setLocale() called persist(), which writes the block tree, unchanged,
    there is nothing to save, and dispatches atelier-refresh, firing a second fetch at the
    exact preview URL the iframe's src change was already navigating to. Both requests carry
    the same session cookie and race each other, and the loser can fail. setLocale() no
    longer calls persist(); the iframe's own navigation is enough.

v0.3.1

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 20 Aug 14:08

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's updatedData() 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 when deleteUploadedFileUsing is 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

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 18 Aug 19:35

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 migrate

Nothing 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 loads routes/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 a booted() 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
    the Organization (or the LocalBusiness subtype you pick), the WebSite, this WebPage
    with its language and dates, a BreadcrumbList derived from nested slugs, ImageObject
    nodes for the logo and share image, and whatever the page is about.

    Nodes reference each other by @id rather 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, being noindex anyway.

    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
    \uXXXX escapes.

  • 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 WebPage node itself, because an About page is a web page.
    Thing-shaped types become their own node linked through mainEntity, 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 sets jobLocationType, without which a
    remote role is filtered out of remote searches. A listing page emits an ItemList of 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 FAQPage automatically,
    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 @id is 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 Organization and WebSite nodes 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

  • Review and AggregateRating from 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.
  • WebSite SearchAction. The sitelinks search box was deprecated in November 2024.

Full changelog: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@Abdulkader-Safi Abdulkader-Safi released this 18 Aug 14:27

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