Changelog
All notable changes to Layup will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
1.5.0 (2026-07-01)
Features
- Add drag from picker to canvas (7bea391)
- add icon picker UI for Blurb widget - Replace plain text input with searchable Select dropdown - 90+ categorized heroicon options with emoji previews - Organized by: arrows, actions, communication, content, business, people, objects, tech, location, time, UI, status (3a3b72b)
- Add inline text editing for text/heading widgets (2451ce0)
- Add recently used widgets to picker modal (afda73d)
- Add revision browser UI to EditPage (4abbb07)
- Add visual column resize handles (9a620bb)
- Add widget icon rendering in picker modal (51826ee)
- core: add option to hide layup "pages" resource in filament panel (f6ddd9a)
- enhance LayupContent with Blade directive, trait cleanup, and tests (c812aeb)
- Improve widget preview in builder canvas (be66dde)
- register 7 missing widgets, remove gap !important hack, add content validation on save (6170a66)
- ui: helper class for layup content rendering (2434337)
- v1.4.0 โ security hardening, safelist bug fix, dedup & polish (db5cd68)
Bug Fixes
- Add live-on-blur validation to widget slideOver forms (7b89d57)
- avatar-group blade handles array and string avatar formats (9d33302)
- blade templates handle array and URL image formats in logo-grid, logo-slider, gallery, masonry, team-grid (d6c97e0)
- Centralize FileUpload disk config for all builder forms (9620cab)
- correct class name typo (dLayupContent -> LayupContent) (b2a0ba1)
- Remove hardcoded rounded-lg from slider, use Design tab border_radius (6ae2aaa)
- Render slider rich content as unescaped HTML (4187fbd)
- restore undo/redo Livewire sync (v1.4.1) (4fb84a2)
- ship stubs/ in Composer dist tarball (d603bb3)
- Slider slides now fill parent height with absolute positioning (7823822)
- ui: page builder with locale key (dbd60ba)
1.4.0 (2026-06-23)
Features
- v1.4.0 โ security hardening, safelist bug fix, dedup & polish (db5cd68)
Bug Fixes
- restore undo/redo Livewire sync (v1.4.1) (4fb84a2)
[1.4.1] - 2026-05-29
Fixed
- Undo/redo (Ctrl+Z / Ctrl+Y) now correctly syncs restored state back to Livewire. Previously
$wire.restoreContent()was called but the method did not exist, so the canvas appeared to undo visually but the field saved the wrong content on form submit. - Column resize drag now adds a history snapshot on release โ resize operations are undoable.
- Inline double-click text edits now push a history snapshot and sync correctly to Livewire state.
- Removed stray
console.logdebug statement in inline edit handler.
[1.4.0] - 2026-05-29
Fixed
- SafelistCollector now correctly collects custom classes and inline styles from section-structured pages (previously only legacy
rowsstructure was scanned).
Security
- Import action path-traversal hardening: the uploaded file path is now validated to reject
.., null bytes, and absolute paths before any disk operation. - Added configurable
layup.uploads.max_size(default 10240 KB) applied to all FileUpload fields to prevent unbounded uploads. ImageWidget::onDeletenow usesconfig('layup.uploads.disk')instead of hardcodedpublicdisk, and validates the path before deletion.- Added
layup.allow_raw_htmlconfig flag (defaulttrue). Set tofalseto escape HTML/embed/map widget output rather than rendering it raw. - Import exceptions are now logged via
logger()->warning()for diagnostics (user-facing message unchanged).
Changed
Pagestatus strings extracted to class constants (Page::STATUS_PUBLISHED,Page::STATUS_DRAFT,Page::STATUS_SCHEDULED).- Sitemap entry priority is now configurable via
layup.seo.sitemap_priority(default'0.7'). SafelistCollector::writeFilenow uses theFilefacade (File::ensureDirectoryExists,File::put) and logs write failures.HasLayupContentandLayupContentnow useContentWalker::extractRows()for all row-extraction paths, eliminating drift between the two implementations.HasLayupContent::buildRowTreenow guards against non-string widget type values (matchesLayupContent).- Scope methods
scopePublished,scopeDraft,scopeSchedulednow have properBuildertype hints and return types.
1.3.1 (2026-05-02)
Documentation
- Reframed docs around the
LayupBuilderfield as the core primitive. Previous docs led with "page builder"; the field-as-primitive story (dropLayupBuilder::make()into any Filament form) was buried. README,docs/introduction.md, anddocs/installation.mdnow lead with the field, with the bundled Pages resource positioned as one application of it. - New
docs/embedding-the-field.md-- end-to-end walkthrough for addingLayupBuilderto any Eloquent model: JSON column migration,HasLayupContenttrait, form integration, and three rendering paths (@layupdirective,toHtml(),<x-layup-widget>). - New
docs/field-only-installation.md-- minimal install path for users who want the field without the bundled Pages resource. Setspages.enabled = false, skips the resource, keeps the safelist + theme + plugin API. - New
docs/customization/filament-plugin-api.md-- complete reference for every fluent method onLayupPlugin(->widgets(),->withoutWidgets(),->withoutConfigWidgets(),->colors(),->darkColors(),->fonts(),->borderRadius(),->withoutPanelColors()). Previously these methods were only listed in API reference. - New
docs/customization/swapping-the-page-model.md-- consolidated guide for using a custom Page model. Lifted the buried "Multiple dashboards" section out ofrendering-content.mdand expanded it with multi-panel patterns and when-to-use-instead-of-embed-the-field guidance. - New
docs/customization/disable-pages-resource.md-- partner doc to field-only install. Documents what disappears whenpages.enabled = false, what still works (field, registry, theme, commands), and optional schema cleanup. - Renamed
docs/advanced/todocs/customization/-- the directory's contents were customization guides; the new name reflects that. All 12 existing files moved viagit mv(history preserved). Internal cross-links and the absolute deployed URL inapi-reference/widget-contract.mdupdated. - Rewrote
docs/customization/_index.mdfrom a 6-line stub into a customization map with an "I want to..." quickref table covering every customization point, including links to embedding the field, plugin API, theme, page templates, SEO, and testing. - Expanded the
SafelistChangedevent documentation indocs/customization/tailwind-safelist.mdwith realistic listener examples (CI rebuild trigger, CDN purge, async job dispatch). - Added "Choose your install path" section to
docs/installation.mdwith both the full Pages-resource path and the field-only path. Inlined aLayupBuilder::make('content')snippet so the core API is visible within the first 30 lines. - Added troubleshooting section to
docs/installation.mdcovering the seven most commonlayup:doctorfailures (plugin registration, missing migrations, storage symlink, layout component,@layupScriptsdirective, safelist file, upload disk) with one-line fix recipes. - Added "Map: which key for which job" intent-grouped table to the top of
docs/configuration.mdso readers can jump to the section they need without scrolling the flat list. - Linked CHANGELOG from
docs/installation.mdfor upgrade guidance.
Notes
- The
docs/advanced/->docs/customization/rename changes the public docs URL from/documentation/layup/v1/advanced/*to/documentation/layup/v1/customization/*. Set up redirects on the docs site (/advanced/* -> /customization/*) before deploying to preserve external links and SEO equity on the old paths. - No code changes in this release. All updates are documentation only. Existing installations require no action.
1.3.0 (2026-05-01)
Added
- Dual-render widget architecture. Widgets can now render through either a Blade view component (the default) or a Livewire component, opt-in via a new
BaseLivewireWidgetbase class. SameWidgetcontract, same editor experience, same registration -- only the frontend render path differs.Crumbls\Layup\View\BaseLivewireWidgetmounts a Livewire component via<livewire:dynamic-component>, passing the widget's$dataas a prop and recursively-rendered children as the default slot. Children remain polymorphic (Blade, Livewire, or any mix).Crumbls\Layup\View\BaseBladeWidgetis the renamed body of the previousBaseWidget. The default Blade rendering path is unchanged.Crumbls\Layup\View\BaseWidgetremains as an abstract alias ofBaseBladeWidgetso every existing widget, stub, doc, and downstream package continues to work without modification.Crumbls\Layup\View\Concerns\HasWidgetDefaultstrait extracts the static metadata defaults shared between both bases.Crumbls\Layup\View\Concerns\Identity\ships per-widget identity traits (HeadingIdentity,ButtonIdentity,NumberCounterIdentity,NewsletterIdentity) so a downstream package can ship a Livewire flavour of those built-ins without redeclaring form schema, defaults, or preview.BaseView::renderChildrenToHtml()recursively renders all children to a single HTML string. Used internally byBaseLivewireWidgetfor the slot path and available to any custom render path.livewire/livewireis a soft dependency (declared incomposer.jsonsuggest). The package loads cleanly without it; install it only when actually usingBaseLivewireWidget.- New
docs/advanced/livewire-widgets.mdcovering when to choose each base, the render flow, the slot-based children model, identity-trait swapping, asset pipeline, performance, testing, and security considerations.
- 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. Crumbls\Layup\Http\Controllers\AbstractControllernow shares the resolved record aslayupPagein view scope, so the component resolves the page automatically on any host layout. Custom controllers can pass:page="$myPage"explicitly.- Page Settings modal exposes Meta Description (160-char) and a "Hide from search engines" toggle (
meta.noindex). - New
layup.seoconfig block:title_suffix,site_name,default_og_image,home_breadcrumb_label. og:locale,og:site_name,article:published_time,article:modified_timeemitted automatically.- BreadcrumbList JSON-LD walks the parent chain with real page titles when a parent_id is set; legacy slug-with-slashes pages still get a path-derived breadcrumb with the page's actual title at the leaf.
tests/Feature/SeoMetaTest.phpadds render-level coverage, including a no-op assertion when<x-layup-seo />runs outside a Layup request.docs/advanced/seo-meta.mddocuments the component, per-page settings, and config knobs.- Nested pages via
parent_id. Pages can now form parent โ child trees. A newpathcolumn stores the resolved URL (cached on save), and the breadcrumb walker uses real ancestor titles instead of slug splitting. Theslugunique index is replaced with a unique index onpath. Migration2026_04_27_000001_add_nesting_to_layup_pages_table.phpadds the columns and backfillspathfromslugfor existing rows.layup.pages.max_depth(default10) caps tree depth and guards against accidental cycles. - New
PageTitleWidget(page-title). A page-aware heading widget that injects the current page's title at render time and reuses the heading widget's view for consistent styling. Useful for templates and shared layouts where the title varies per page. - Scheduled publishing. Pages with
status = 'scheduled'and a futurepublished_atautomatically flip topublishedonce their publish time arrives. Thelayup:publish-scheduledconsole command does the work (with a--dry-runflag) and the service provider auto-registers it on the app's scheduler to run every minute. Newlayup.scheduling.auto_publishconfig (defaulttrue) lets hosts opt out and register the command themselves (e.g. on a single worker). layup.pages.enabledconfig option to register or hide the bundledPageResourcein the Filament panel. Set tofalseto use Layup as a rendering engine without surfacing the admin UI; pages still render normally via the frontend controller,@layupdirective, orHasLayupContenton host models. (Thanks @TheGodlyLuzer, #20.)
Changed
BaseView::render()return type widened fromIlluminate\Contracts\View\ViewtoView|Htmlable|string. Existing Blade widgets are unaffected (returning aViewstill satisfies the wider type via covariance); the wider type letsBaseLivewireWidget::render()return astringfromBlade::render(). Subclasses that previously declared: Viewcontinue to work.- Internal type checks switched from
instanceof BaseWidget/is_subclass_of(..., BaseWidget::class)to interface-basedinstanceof Widget/is_subclass_of(..., Widget::class)inCrumbls\Layup\Support\Concerns\RegistersWidgets,Crumbls\Layup\Support\LayupContent,Crumbls\Layup\Testing\LayupAssertions, andtests/Unit/WidgetDefaultCompletenessTest.php. Downstream code that introspects widgets at runtime should follow suit if it intends to support Livewire widgets, but no immediate change is required for code that only sees the Blade lineage. Crumbls\Layup\Testing\LayupAssertions::assertWidgetRenders()andassertWidgetRendersWithDefaults()now accept anyView|Htmlable|stringfromrender()(a newrenderToString()helper coerces the result). Tests that previously called$widget->render()->render()directly continue to work because Blade widgets still return aView.- Four built-in widgets (
HeadingWidget,ButtonWidget,NumberCounterWidget,NewsletterWidget) refactored to consume identity traits. Public surface unchanged -- all static methods resolve identically. The remaining ~95 built-in widgets are untouched. - SEO meta is now emitted on every published page. Previously the entire block was gated on
meta.descriptionbeing set, so pages without a description silently dropped all SEO meta. - Twitter card type is now
summary_large_imagewhen a featured image is present,summaryotherwise. Was hardcoded tosummary. og:typeis nowarticlefor pages withpublished_at,websiteotherwise. Was always implicitlywebsite.getMetaTitle()honorslayup.seo.title_suffix.getFeaturedImageUrl()falls back tolayup.seo.default_og_imageso social shares always have an image.- Removed the always-hidden SEO
SectionfromPageResource::form(); description editing now lives in the Page Settings modal where it is actually visible. - Bundled reference layout (
resources/views/layouts/page.blade.php) renders<x-layup-seo />instead of an undocumented$metaslot. The previous slot contract has been removed entirely; hosts whose layouts rendered{{ $meta ?? '' }}should swap it for<x-layup-seo />(the component is the only supported integration point).
Fixed
- Editor-set page descriptions and Open Graph data now reach rendered HTML on host layouts. The previous slot pattern required host layouts to opt into an undocumented
metaslot, and the bundled reference layout itself never rendered it โ so vendor-publishing the layout produced zero SEO output regardless of editor input. Replacing the slot with a drop-in component eliminates the silent-failure mode. - Backfill missing top-level defaults across 12 widgets (22 fields) so freshly inserted widgets render with complete data. Previously some fields were declared in the form schema but absent from
getDefaultData(), producing undefined-key warnings on first render until the editor saved the widget once. layup:doctorno longer flags Repeater and Builder sub-fields as missing top-level defaults. The walker now distinguishes nested fields (whose defaults live in the parent'sdefault()payload) from top-level fields, eliminating a class of false positives.- Revision restore in the page builder produces a cleaner diff view and correctly re-applies revision content without losing widget IDs.
Maintenance
bin/docs-lintis restored and hardened against three edge cases that previously masked broken links and stale anchors during local doc builds.
1.2.3 (2026-04-19)
Fixed
layup:installfailing with "Failed to open stream: No such file or directory" when installed from Packagist dist. The/stubsdirectory was markedexport-ignorein.gitattributes, stripping the stub files (app-layout.blade.php.stub,layup-widget.php.stub,layup-widget-view.blade.php.stub,layup-widget-test.php.stub) from the distributed tarball.InstallCommand,MakeWidgetCommand, andLayupServiceProvider::boot()all reference these stubs at runtime, so they must ship with the package.
1.2.2 (2026-04-05)
Added
- Full documentation site in
docs/following Spatie conventions (30 pages across 6 sections) - Getting Started guide: creating pages, adding widgets, saving and publishing
- Widget reference pages for all 5 categories (Content, Media, Interactive, Layout, Advanced)
- Configuration reference with all
config/layup.phpoptions - Grid system documentation (12-column grid, responsive spans, breakpoints, visibility)
- Advanced guides: custom widgets, extending widgets, Tailwind safelist, revision history, page templates, frontend rendering, theme system, testing helpers, rendering content
- API reference: Widget contract, models, service provider, support classes
.gitattributesto exclude docs, tests, and dev files from Composer dist installshomepageandsupportURLs incomposer.jsonfor Packagist
Changed
- README trimmed to concise overview with links to full documentation
- Installation section updated with
layup:installwizard and Filament prerequisite
1.0.6 (2026-03-10)
Bug Fixes
- correct class name typo (dLayupContent -> LayupContent) (b2a0ba1)
1.0.5 (2026-03-10)
Bug Fixes
- ui: page builder with locale key (dbd60ba)
1.0.3 (2026-03-08)
Bug Fixes
- Add live-on-blur validation to widget slideOver forms (7b89d57)
- Centralize FileUpload disk config for all builder forms (9620cab)
- Remove hardcoded rounded-lg from slider, use Design tab border_radius (6ae2aaa)
- Render slider rich content as unescaped HTML (4187fbd)
- Slider slides now fill parent height with absolute positioning (7823822)
1.2.1 (2026-03-29)
Added
- Global theme system with CSS custom properties (
--layup-primary,--layup-secondary,--layup-accent,--layup-success,--layup-warning,--layup-danger) - Semantic theme variables:
--layup-on-{color}(auto-contrast),--layup-surface,--layup-on-surface,--layup-border,--layup-mutedwith light/dark variants - Dark mode theme support with automatic color lightening and
->darkColors()manual overrides - Custom
ColorPickerform field with theme-aware swatches and native color picker fallback - Theme color configuration via
LayupPlugin::make()->colors(),->darkColors(),->fonts(),->borderRadius() - Filament panel color inheritance (automatic, opt out with
->withoutPanelColors()) ThemeResolverensures theme is hydrated on frontend routes where Filament panels don't boot- 19 new tests for LayupTheme (dark colors, auto-lightening, CSS output) and ColorPicker field
- Mobile-responsive layouts for all 47 widget blade templates
- Theme system documentation in README with full API reference
Changed
- All 37 widget color fields replaced with new
ColorPickercomponent (swatches + custom picker) - All hardcoded hex color defaults in widget PHP classes set to
null; Blade views fall back to CSS variables - All hardcoded Tailwind blue, green, red, and yellow classes replaced with theme CSS variable equivalents
- Alert, highlight-box, badge, changelog variants now derive from
--layup-success,--layup-warning,--layup-danger - Star ratings, checkmarks, required asterisks, success messages all use theme variables
- Cookie consent uses
--layup-on-secondaryfor contrast-safe text - Testimonial border uses inline style for overridability instead of
layup-border-primaryclass - Gradient text defaults to
--layup-primary/--layup-accentinstead of hardcoded purple/blue - Grids (feature-grid, gallery, logo-grid, team-grid, metric, post-list, pricing-toggle, masonry, text-columns) collapse to 1-2 columns on mobile via scoped media query style blocks
- Flex layouts (hero buttons, blurb, step-process, icon-box, search, file-download, cookie-consent) stack vertically on mobile
- Heading sizes scale down responsively (h1:
text-2xl md:text-4xl, h2:text-xl md:text-3xl, etc.) - Padding reduced on mobile across banner, CTA, hero, slider, testimonials, flip-card, image-card, tabs, table cells
- Hotspot/image-hotspot tooltips capped to viewport width on mobile
- Lottie widget uses
max-width+width: 100%instead of fixed width FieldPacks::colorPair()andFieldPacks::hoverColors()now useColorPickerinstead ofTextInput
Fixed
-
Theme colors not loading on frontend routes (panel boot never fires outside admin)
-
Info callout text unreadable when using primary color as body text (now uses
--layup-on-surface) -
75 built-in widgets across Content, Media, Interactive, Layout, and Advanced categories
-
Flex-based 12-column grid with responsive breakpoints (sm/md/lg/xl)
-
Visual span picker for click-to-set column widths per breakpoint
-
Drag & drop reordering for widgets and rows
-
Undo/Redo with full history stack (Ctrl+Z / Ctrl+Shift+Z)
-
Searchable, categorized widget picker modal
-
Three-tab form schema (Content / Design / Advanced) on every component
-
Full Design tab: text color, alignment, font size, border, radius, shadow, opacity, background, padding, margin
-
Responsive visibility: show/hide per breakpoint on any element
-
Entrance animations: fade in, slide up/down/left/right, zoom in (via Alpine x-intersect)
-
Frontend rendering with configurable routes, layouts, and SEO meta
-
Tailwind safelist generation with auto-sync on page save
-
Page templates: 5 built-in + save your own
-
Content revisions with auto-save and configurable max
-
Export/Import pages as JSON
-
Widget lifecycle hooks:
onSave,onCreate,onDelete -
Content validation (structural + widget type)
-
Widget auto-discovery from
App\Layup\Widgets -
Configurable Page model per dashboard
-
Blurb icon picker with 90+ searchable Heroicons
-
make:layup-widgetArtisan command -
Pint + Rector for code quality
-
Pre-push hook running Pint and Pest
Changed
- Editor CSS restyled to match Filament's native look (flat rows, dashed columns, elevated widget cards)
- Dark mode support via Filament CSS custom properties
[0.1.0] - 2026-02-24
Added
- Initial development release