github Crumbls/layup v1.3.0
Dual-render widgets (Blade or Livewire per widget), drop-in SEO meta component, nested pages, scheduled publishing, and a panel toggle to use Layup as a pure rendering engine.

3 hours ago

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

  1. Run migrationsphp artisan migrate. The new migration adds parent_id and path columns to layup_pages, drops the unique-on-slug index, and backfills path from 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.
  2. Replace the $meta slot 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.
  3. (Optional) Configure SEO defaults. If you publish the config, the new layup.seo block lets you set title_suffix, site_name, default_og_image, and the breadcrumb home label.
  4. (Optional) Wire scheduled publishing. Make sure your app scheduler is running (schedule:work in dev, the standard cron in production). To wire layup:publish-scheduled yourself, set
    layup.scheduling.auto_publish to false.
  5. 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 from View to View|Htmlable|string. Existing Blade widgets still satisfy the wider type via covariance.
  • Internal type checks moved from instanceof BaseWidget to interface-based instanceof Widget in RegistersWidgets, 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() and assertWidgetRendersWithDefaults() accept any View|Htmlable|string from render() via a new renderToString() 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:doctor no 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

Don't miss a new layup release

NewReleases is sending notifications on new releases.