Headline features
Dual-render widgets — Livewire support
Widgets can now render through either a Blade view component (the default) or a Livewire component, opt-in via a new BaseLivewireWidget base class. Same Widget contract, same editor experience, same
registration — only the frontend render path differs. BaseWidget stays as a back-compat alias of the renamed BaseBladeWidget, so every existing widget, stub, and downstream package continues to work
without modification. livewire/livewire is a soft dependency — install it only when you actually use BaseLivewireWidget. New identity traits (HeadingIdentity, ButtonIdentity,
NumberCounterIdentity, NewsletterIdentity) let downstream packages ship Livewire flavours of built-ins without redeclaring schema, defaults, or preview. See docs/advanced/livewire-widgets.md.
Drop-in SEO meta component
New <x-layup-seo /> Blade component. Drop it once into your layout's <head> and Layup emits the full meta block (description, OG, Twitter, canonical, robots, article timestamps, JSON-LD) on every
layup-rendered request. On non-layup routes the component renders nothing, so it's safe in shared layouts. The Page Settings modal now exposes Meta Description and a "Hide from search engines" toggle. New
layup.seo config block adds title_suffix, site_name, default_og_image, and home_breadcrumb_label. BreadcrumbList JSON-LD now walks the parent chain with real page titles.
Nested pages
Pages can now form parent → child trees. A new path column stores the resolved URL (cached on save) and the unique constraint moves from slug to path — so two pages can share a slug under different
parents. Migration 2026_04_27_000001_add_nesting_to_layup_pages_table.php adds the columns and backfills path from slug for existing rows. layup.pages.max_depth (default 10) caps tree depth.
Scheduled publishing
Pages with status = 'scheduled' and a future published_at flip to published once their publish time arrives. The layup:publish-scheduled console command (with a --dry-run flag) does the work, and
the service provider auto-registers it on the app's scheduler to run every minute. Set layup.scheduling.auto_publish to false to wire it yourself (e.g. on a single dedicated worker).
Panel toggle: use Layup as a rendering engine
New layup.pages.enabled config option registers or hides the bundled PageResource in the Filament panel. Disable it when you're using Layup purely as a rendering engine — attaching HasLayupContent to
your own models and managing content through your own resources. Frontend rendering, the @layup directive, and existing pages are unaffected. Thanks @TheGodlyLuzer
(#20).
Bonus: PageTitleWidget
A new built-in widget that renders the current page's <h1> at render time, perfect for shared templates and layouts. Brings the built-in count to 96.
Upgrading from 1.2.x
- Run migrations —
php artisan migrate. The new migration addsparent_idandpathcolumns tolayup_pages, drops the unique-on-slug index, and backfillspathfrom existing slugs. Pages whose
slug already contains slashes continue to work; their breadcrumb falls back to path-derived segments until you split them into real parent/child rows. - Replace the
$metaslot with the SEO component. If your host layout used{{ $meta ?? '' }}in the<head>, swap it for<x-layup-seo />. The slot contract is removed entirely — the component is
the only supported integration point. Hosts on the bundled reference layout (resources/views/layouts/page.blade.php) get the swap automatically. - (Optional) Configure SEO defaults. If you publish the config, the new
layup.seoblock lets you settitle_suffix,site_name,default_og_image, and the breadcrumb home label. - (Optional) Wire scheduled publishing. Make sure your app scheduler is running (
schedule:workin dev, the standard cron in production). To wirelayup:publish-scheduledyourself, set
layup.scheduling.auto_publishtofalse. - Livewire is opt-in. No action needed unless you want to use
BaseLivewireWidget; existing widgets continue to render through Blade.
Notable internal changes
BaseView::render()return type widened fromViewtoView|Htmlable|string. Existing Blade widgets still satisfy the wider type via covariance.- Internal type checks moved from
instanceof BaseWidgetto interface-basedinstanceof WidgetinRegistersWidgets,LayupContent,LayupAssertions, and the default-completeness test. Downstream code
that introspects widgets at runtime should follow suit if it intends to recognise Livewire widgets. LayupAssertions::assertWidgetRenders()andassertWidgetRendersWithDefaults()accept anyView|Htmlable|stringfromrender()via a newrenderToString()helper.
Fixes
- Editor-set page descriptions and Open Graph data now reach rendered HTML on host layouts (root cause of the SEO component work).
- Backfilled missing top-level defaults across 12 widgets (22 fields) so freshly inserted widgets render with complete data.
layup:doctorno longer flags Repeater/Builder sub-fields as missing top-level defaults.- Revision restore produces a cleaner diff view and re-applies content without losing widget IDs.
Stats
- 1,168 tests, 3,649 assertions (up from 1,131 / 3,517).
- 96 built-in widgets (added
PageTitleWidget).
Full changelog: v1.2.3...v1.3.0