Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cms Laravel Package

tallcms/cms

TallCMS adds a modern CMS to Laravel Filament: pages, posts, block-based editor, media library, menus, comments, and forms. Install via Composer and register the TallCmsPlugin to turn an existing Filament app into a full CMS.

View on GitHub
Deep Wiki
Context7
v4.5.2

Bug Fixes

  • Hero block: Fix background-attachment: fixed rendering incorrectly on iOS Safari, where the background image appeared zoomed/cropped because iOS sizes fixed backgrounds to the viewport instead of the element. The Hero block's background div now opts into the same media-query fallback already used by the Parallax block — parallax stays active on desktop, but switches to scroll on touch devices, small viewports, and prefers-reduced-motion. Samsung Internet and desktop browsers are unaffected.
v4.5.1

Performance

  • Cache the resolved menu tree per location / locale / host so repeated menu() calls don't rebuild the same structure on every request. Active-item state is overlaid per-request so the cached tree still highlights correctly for the current URL. (#96)
  • Add a MenuCache helper that uses tagged cache when the store supports it, with a versioned-key fallback for non-tag stores. (#96)
  • Invalidate the menu cache on TallcmsMenu / TallcmsMenuItem save/delete, on CmsPage slug / parent / homepage changes, and on writes to URL-affecting site settings (site_type, i18n_enabled, default_locale, hide_default_locale, i18n_locale_overrides). (#96)
  • Memoize site setting lookups within a request and negative-cache missing values so absent settings don't re-query on every read. (#96)
  • Clear request-scoped and static site-name memos when site_name is written, so subsequent reads in the same request (and across requests on long-running workers) return the new value. (#96)

Reported timings on a large CMS page: ~1.7–2.0s → ~240–260ms with Redis as the persistent cache store.

Bug Fixes

  • Fix ImageColumn missing disk() in CmsPages and CmsPosts tables, with regression tests. (#93)

Dependencies

  • Bump axios from 1.15.0 to 1.15.2. (#95)

Documentation

  • Restructure the multisite setup guide as a layered SaaS setup flow
  • Link multisite, billing, and architecture guides from the docs README
  • Drop the manual migrate step from multisite install instructions
  • Document the Rich Editor Phase 2 surface in ref-rich-editor.md

Compatibility

No breaking changes. Cache invalidation is automatic; the menu cache requires a persistent cache store (Redis, file, database) to retain benefit across requests — the array driver only persists within a single request.

Contributors

Thanks to the following contributors whose work shipped in this release:

  • @VaclavKlima — performance caching work (#96), first-time contributor 🎉
  • @jakublackoImageColumn disk fix (#93)
  • @dependabot — automated dependency updates (#95)
v4.5.0

Rich Editor Phase 2

This release lands a six-PR overhaul of the CMS Rich Editor focused on managing long structured pages with many blocks. The core problems it solves: hard-to-grab tall blocks, no document overview, picker that scrolls away, and no fast keyboard insertion.

What's new

Sticky side panel (#86) — the block panel docks to the viewport on long pages instead of scrolling away with the document. Desktop-only (≥1024px) so narrow admin layouts don't get trapped scroll. Configurable top offset via --tallcms-editor-sticky-offset for hosts with their own sticky topbar.

Picker polish (#87) — multi-word AND search (form contact finds Contact Form), result ranking by match quality, and a "Recently used" row at the top of the picker backed by localStorage. Drag insertion now also counts toward recents.

Per-block hover chrome (#88) — every custom block in the editor gets a floating mini-toolbar on hover: drag handle, move up, move down, duplicate. Solves the "can't grab a 800px Hero" problem with a small predictable hit target. Reorder commands transact in a single step (clean undo).

Outline tab (#89) — a second tab in the side panel showing every customBlock in the document as a drag-reorderable list. Click to scroll-to, drag to reorder. Heuristic title extraction pulls the actual heading text from each block's config (Hero "Welcome to my site" instead of just "Hero"). Reorders run in one TipTap transaction via a generalized moveCustomBlockTo command.

Per-block collapse (#90) — fifth chrome button folds a block's preview into a one-line summary card so you can scan past it. Header and chrome stay visible. State persists across panel/tab switches, resets on page reload (per the "opt-in, see how it feels" design call).

Slash commands (#91) — type / anywhere in the editor for a Notion-style block picker. Filters as you type using the same multi-word search as the side panel. Arrow keys + Enter to insert. Escape or click-away to close. A default placeholder hints at it on a fresh editor: "Type / for blocks, or use the side panel".

Implementation notes

  • New TipTap extension shipped via BlockChromePlugin (a Filament RichContentPlugin). Three ProseMirror plugins inside it: BlockChromeView, OutlineSyncView, SlashCommandView. Bundle (~198KB) is loaded on-request — pages without an editor pay nothing.
  • Three new TipTap commands operating on customBlock nodes: moveCustomBlockUp(pos), moveCustomBlockDown(pos), moveCustomBlockTo(fromPos, toIndex), duplicateCustomBlock(pos). All accept explicit positions resolved from DOM at click time, so reorders survive intermediate edits.
  • Outline ↔ panel coupling stays event-shaped via scoped cms-block-outline-changed and cms-block-action events on editor.dom — no direct references between layers, so multiple editors on a page never cross-talk.
  • Built-on works for both standalone and plugin-mode installs (sticky styles moved into the package CSS so plugin-mode adopters get them via FilamentAsset).

Known follow-ups (not in this release)

  • Outline-tab title truncation on very long titles — the :title tooltip provides a fallback for now
  • Handle-only dragging (currently the entire block stays draggable; the handle is additive affordance)
  • "Structure mode" toggle that auto-collapses tall blocks (the per-block opt-in is the foundation; this is the smart-defaults follow-up)
  • Slash menu enhancements: recently used, categories
  • Identifier regeneration on duplicate for blocks that store user-facing IDs in config

Compatibility

No breaking changes. Existing CmsRichEditor usage continues to work unchanged. Custom block packages are unaffected.

v4.4.9

Highlights

Block grid normalization and per-site theme UI control toggles.

Block grids and theme polish (#83)

  • Consistent column ramp across all grid-capable blocks (Features, Team, Testimonials, Posts, Pricing, Stats, Logos): mobile 1 → sm 2 → lg N. Fixes inconsistent tablet behavior and Logos' reversed scaling on 4/5/6 columns.
  • Stats block: replaced daisyUI's stats stats-vertical lg:stats-horizontal wrapper (which uses grid-auto-flow: column and ignored grid-cols-N) with a plain Tailwind grid that respects the configured column count.
  • Strict-match bug: cast columns to string in match expressions across Stats / Testimonials / Posts / Pricing / Logos (and standalone Features). Saved configs sometimes stored the value as integer; PHP match strict equality made integer 2 fall through to default and render the wrong column count.
  • Theme tokens: replaced hardcoded bg-white / text-gray-500 colors in Parallax CTA and Document List with daisyUI tokens so they respect dark and branded themes.
  • A11y: added aria-labels to the close / prev / next icon-only buttons in the Image Gallery lightbox.

Per-site theme UI toggles (#85)

  • New per-site SiteSetting toggles let admins hide the theme switcher, search box, or language dropdown without editing Blade.
  • Surfaced in the Theme Manager slide-over alongside the Delete action, gated on the active theme so toggles only appear for controls the theme declares it actually renders (theme.json supports.theme_controller, supports.search, supports.language_switcher).
  • Auto-save on toggle, multisite-aware (per-site overrides win over Global Defaults). Defaults stay true so existing installs keep current rendering on upgrade.
  • "Configurable" hint icon on the Active badge for theme cards whose active theme has at least one display option.
  • make:theme gains prompts and --include-search / --include-language-switcher flags to scaffold matching component stubs and theme.json supports flags.
  • Bundled TallDaisy and Elevate themes updated to honor the new toggles.

Upgrading

No migrations. No config changes required. All three new SiteSetting defaults are true, so existing installs render exactly as before until an admin flips a toggle.

v4.4.8

Bug Fix / UX

When a user enters the page editor from a Site relation manager (Sites → Site → Pages → Edit or Create), the receiving Filament page now keeps the originating-site context — instead of dropping the user into the global Pages resource with no breadcrumb back.

  • New HasFromSiteContext trait captures ?from_site=<id> once at mount() into a public Livewire property — durable across /livewire/update round trips, so save / delete redirects don't silently lose context.
  • EditCmsPage and CreateCmsPage now show "Editing within site: " as a subheading, expose a "Back to site" header action, and redirect after save / delete / force-delete to the originating Site edit URL instead of the global Pages index.
  • CmsPage::hasSiteIdColumn() exposed as public; site() belongsTo relation added; 'site_id' => 'integer' cast added so loose Eloquent typing doesn't break validation.

Safety

  • Single-site installs: trait's first guard checks CmsPage::hasSiteIdColumn(). When the multisite migration hasn't added the column, every entry point returns null and lifecycle hooks fall through. Behaviour is identical to before.
  • Spoofed from_site: validated against the loaded record's site_id post-save (with a fallback to $ownerSiteId pre-save so the back action renders on Create before Save). A mismatched id falls through to default behaviour — no breadcrumb leak.
  • SiteResource URL resolution: try/catch around getUrl() on cms-core's SiteResource and the multisite plugin's SiteResource in turn. Hosts without either resource registered get null and the back action silently drops out — no RouteNotFoundException reaches the user.

Companion plugin release

The multisite plugin v2.3.0 (forthcoming) ships the ?from_site=<id> query addition on its PagesRelationManager. Hosts running the multisite plugin should update both core (this release) and the plugin to see the full UX. Either side can ship independently — old plugin + new core: trait stays inert because from_site is never sent. New plugin + old core: query param is ignored. No breakage in either combination.

v4.4.7

Bug Fixes

Two issues caused the frontend runtime (public/vendor/tallcms/tallcms.js) to drift out of sync with the package on existing hosts. After v4.4.7 they self-heal on every composer update.

  • Auto-publish on composer update: The cms package's compiled assets are now also registered under Laravel's conventional laravel-assets publish tag. The standalone scaffold's standard post-update-cmd hook runs vendor:publish --tag=laravel-assets --force on every composer update, so any cms upgrade that ships a new tallcms.js / tallcms.css automatically refreshes public/vendor/tallcms/. Previously the package only registered tallcms-assets, which had to be invoked manually.
  • Cache-bust the runtime URL: [@tallcmsCoreJs](https://github.com/tallcmsCoreJs) now appends the published bundle's mtime as a ?v= query string. Browsers automatically pick up the new bundle on the next request after a cms upgrade — no more "I republished but the form still fails because my browser had the old JS cached."

Why it matters

The _page_url_pageUrl rename in the contact-form runtime broke contact submissions on hosts whose public/vendor/tallcms/tallcms.js hadn't been republished, even though Composer pulled the new dist into vendor/. Both fixes together mean the next runtime contract change of this kind propagates without manual ops steps.

v4.4.6

Bug Fixes

  • Timeline block: Fixed View [cms.blocks.partials.timeline-content] not found error in plugin-mode installs. The block's [@include](https://github.com/include) calls were missing the tallcms:: namespace prefix, so partial views resolved only on standalone installs (which have a copy under the default namespace) and failed everywhere else.
  • Hero block (with-form layout): Primary and secondary CTA buttons now render alongside the form card. Previously they were hidden in this layout on the assumption that the form submit was the sole CTA, but real-world heroes often want both — e.g. lead-capture form plus a WhatsApp or live-demo button. Applied to both the default hero view and the Elevate theme variant.

Operability

  • Contact form rejections: The "Contact form submission with invalid signature" warning now logs non-sensitive fingerprints (config hash + signature prefixes) so operators can tell whether multiple rejected requests share the same payload — distinguishing intermittent tampering from systemic mismatches like APP_KEY rotation or stale page-cache.
v4.4.5

Summary

Patch release. Closes a Shield-gating gap on the ThemeManager page where five installation-wide actions (delete, upload, license activate / deactivate / refresh) were available to any user with `View:ThemeManager` permission — including site_owner roles who legitimately need that permission to switch their own site's active theme.

Same pattern as v4.4.4's RegistrationSettings fix and tallcms/pro v1.8.2's ProSettings fix, but at the action level rather than the page level: `HasPageShield` correctly gates page access, but the destructive actions on the page had no per-action authorization.

Actions gated to super_admin

Action Was Now
`delete` Open to anyone with view permission super_admin only
`upload` Gated only on `theme.allow_uploads` config flag super_admin AND `theme.allow_uploads`
`activateLicense` Open to anyone with view permission super_admin only
`deactivateLicense` Open to anyone with view permission super_admin only
`refreshAllLicenses` Open to anyone with view permission super_admin only

Each gate has three layers:

  1. `->visible(...)` — UI hide
  2. `->authorize(...)` — backend block for direct AJAX invocation
  3. Defensive role check + `Notification` fallback inside the action body

All gates use `auth()->user()->hasRole('super_admin')`. Single-site installs continue to work because tallcms's install command marks the first user as super_admin.

Impact for site owners

No regression. Site owners on multisite installs (and any non-super-admin role with `View:ThemeManager`) lose access to actions they shouldn't have had in the first place. They can still:

  • See the list of installed themes
  • Switch their own site's active theme
  • View theme details

Pull request

  • #80 — Gate ThemeManager destructive + license actions to super_admin

Pattern note

This is the same kind of gap as v4.4.4 RegistrationSettings (page level). The broader pattern — page-level Shield being insufficient once any role with valid view permission exists — is worth a once-over across the rest of the admin panel. Likely candidates are pages that mix editor-facing content with installation-admin actions. Worth a focused audit follow-up.

Upgrading

Standard `composer update`. No DB migration. Audit role assignments after deploy if you have non-super-admin roles with `View:ThemeManager` — they'll lose the destructive action buttons but keep their view + activate-on-own-site capabilities.

v4.4.4

Summary

Patch release that closes a Shield-gating gap on the registration settings page. The `tallcms/filament-registration` package ships its `RegistrationSettings` page intentionally without Shield (so the package stays Shield-optional). Without further wiring, that page sat on the admin panel accessible to anyone who could log in — `View:RegistrationSettings` permission rows in the DB were unread.

Same shape as the v1.8.2 ProSettings fix in tallcms/pro.

What changed

  • `composer.json` requires tallcms/filament-registration ^1.2 (1.2.0 added the `settingsPage()` setter for swapping in a Shield-gated subclass).

  • New `App\Filament\Pages\RegistrationSettings` subclass adds `HasPageShield`.

  • `AdminPanelProvider` wires it via the bridge plugin's new `settingsPage()` forwarder:

    ```php TallcmsRegistrationBridge::make() ->settingsPage(\App\Filament\Pages\RegistrationSettings::class) ```

Required: bridge plugin upgrade to 2.1.0

The host wiring assumes the bridge plugin (`tallcms/registration`) at v2.1.0+ because that's where `RegistrationPlugin::settingsPage()` was added. Older bridge versions (2.0.0) will throw `BadMethodCallException` on panel boot.

If you have the bridge plugin installed:

  1. Upgrade it to v2.1.0 via the admin's Plugin Manager (uploading the new zip), or pull the new version from the marketplace.
  2. After upgrade, run `php artisan shield:generate` to seed the `View:RegistrationSettings` permission for existing roles.
  3. Audit role assignments — site_owner / site_user / similar roles need `View:RegistrationSettings` granted explicitly if they should keep access.

If you don't have the bridge plugin installed (e.g. plugin-mode users, fresh CI checkouts), the `class_exists` guard in `AdminPanelProvider` skips the bridge cleanly — no boot failure.

Companion release

Same fix shipped to push.sg (`tallcms/push-tallcms`) in commit `eb51940` — push.sg uses `FilamentRegistrationPlugin` directly without the bridge plugin, so its wiring is one-call:

```php FilamentRegistrationPlugin::make() ->defaultRole('site_owner') ->settingsPage(\App\Filament\Pages\RegistrationSettings::class) ```

Pull request

  • #79 — Shield-gate the RegistrationSettings page

Note on cms package contents

The cms package (`packages/tallcms/cms/`) itself has no functional changes between v4.4.3 and v4.4.4 — all changes ride on the standalone skeleton (`app/Filament/Pages/`, `app/Providers/Filament/AdminPanelProvider.php`, `composer.json`). The version bump exists so the standalone bundle ships via the standard release flow.

v4.4.3

Highlights

Adds an explicit dashboard scope picker so admin dashboard widgets honour a real, visible "current site" selection — fixing the bug where widgets always rendered the role-based fallback site (super_admin → default site, others → first owned site) regardless of which site the user thought they were on.

Backwards compatible: the picker seeds itself using the same fallback the widgets already resolve, so first dashboard load after upgrade matches what users were seeing before — they just now have a visible scope control they can change.

What's new

DashboardSitePicker widget

New TallCms\Cms\Filament\Widgets\DashboardSitePicker registered at the top of the admin dashboard. Renders a select with the user's accessible sites (super_admins also see "All Sites"). On selection it writes session('multisite_admin_site_id') and dispatches the dashboard.site-changed Livewire event so other widgets refresh in place — no full page reload.

Server-side authorization

Every selection (mount + updatedSelected) is validated against the current user's access via resolveAuthorizedSiteValue():

  • __all_sites__ is only valid for super_admins.
  • A numeric site_id must reference an active site that the user owns (super_admins can pick any active site).
  • Tampered Livewire input is rejected without dispatching the event.
  • Stale unauthorized session values (e.g. __all_sites__ left over from a previous super_admin login on a shared browser) are normalized on mount.

HasMultisiteWidgetContext trait

New shared trait at TallCms\Cms\Filament\Widgets\Concerns\HasMultisiteWidgetContext. Lifts the byte-for-byte duplicated getMultisiteSiteId() / getMultisiteName() helpers out of MenuOverviewWidget + ContentHealthWidget and adds isAllSitesSelected() for branching on the all-sites case. Both core and Pro widgets consume it.

Cms widgets refresh on scope change

MenuOverviewWidget and ContentHealthWidget now adopt the trait and add #[On('dashboard.site-changed')] listeners so they re-render when the picker fires.

Why a separate picker (not just wiring SiteSwitcher to write session)

SiteSwitcher::navigateToSite() (SiteSwitcher.php:47-51 comment) deliberately doesn't mutate session because authorization there flows through SitePolicy and content scopes through the Site resource's RelationManagers. Hijacking it would conflict with that design and ripple into every consumer of the same session key (ProSetting::resolveCurrentSiteId, PluginLicense::findForCurrentContext, redirect manager, etc.).

The picker keeps both concepts honest: SiteSwitcher = navigate; DashboardSitePicker = scope.

All Sites semantics

Widget All Sites behaviour
MenuOverviewWidget Show global counts (sum across all sites)
ContentHealthWidget Show global counts
AnalyticsOverviewWidget (Pro 1.10.0) Empty-state message — analytics is per-site
LicenseStatusWidget (Pro 1.10.0) Empty-state message — Pro licenses are per-site

The asymmetry is intentional: count(*) is a meaningful aggregate for cms widgets, but per-site provider data (Plausible/Fathom) and per-site Pro licenses don't aggregate cleanly without provider-side fan-out.

Testing

  • Trait unit suite: 11 assertions covering each branch of getMultisiteSiteId (specific id, all-sites sentinel, super_admin default-site fallback, regular-user first-owned fallback, QueryException safety net), getMultisiteName, and isAllSitesSelected.
  • Picker behaviour + authorization: 11 Livewire tests (default-site seeding, regular-user single-owned-site seeding, explicit selection + event dispatch, can_view truthiness, regular user cannot select another user's site / __all_sites__ / inactive site, mount normalizes stale foreign-user session value, mount rejects stale __all_sites__ for non-super-admin).
  • Pro widget render tests in standalone (10 tests, 20 assertions): All Sites empty state, specific-site rendering, setPeriod/refreshData short-circuiting on All Sites, event-driven data clearing, License heading and sentinel status.

Pull request

  • #78 — Add DashboardSitePicker so widgets honour an explicit site scope

Pro plugin

tallcms/pro 1.10.0 (separately released) ships site-aware Analytics + License widgets that consume the new trait, listen for dashboard.site-changed, render an empty-state message under __all_sites__, and honour the picker as the source of truth. Pins compatibility.tallcms: >=4.4.3.

Upgrading

No code changes required. The dashboard will gain a "Dashboard scope" picker at the top; first load picks up the same role-based fallback site the widgets were already showing, so existing users see exactly what they saw before — until they pick a different site.

Note on direction

This release makes the existing session-driven model honest, so super_admins can see and change the dashboard's site scope. It is not a final multisite analytics architecture — provider keys/secrets are still configured globally on the Pro Settings page. A follow-up will move per-site analytics configuration into the Site resource's edit page (alongside Branding, Embed Code, etc.) so analytics provider, site key, and secret are explicitly per-site.

v4.4.2

🙏 Thanks to @jakublacko

This release is shipped almost entirely thanks to a thorough community contribution from [@jakublacko](https://github.com/jakublacko) in #71. Beyond the headline feature, the PR uncovered and fixed a string of latent bugs in the media library subsystem that had been quietly degrading API uploads and the edit-media flow. The PR also came with 600+ lines of new test coverage. Excellent work — much appreciated.

New: configurable media disk

  • New env var TALLCMS_MEDIA_DISK lets installs route media to any filesystem disk defined in config/filesystems.php (custom-named S3 buckets, R2, local, etc.) instead of TallCMS's auto-detection between s3 and public.
  • The media disk is now centralised through cms_media_disk(), so every controller, page, and resource in the media flow consults the same source of truth.
  • cms_uses_s3() now does driver-based detection instead of literal-name matching, so disks named anything other than 's3' are correctly recognised when their underlying driver is S3.

Set TALLCMS_MEDIA_DISK=my-bucket in .env and TallCMS will use that disk for all new media operations. Leave it unset for the existing auto-detect behaviour.

Bug fixes

  • API media uploads silently dropped image dimensions. MediaController::store() was writing width/height as top-level columns, but those aren't $fillable — and the model reads them from meta['width']/meta['height'] via accessors. Dimensions now persist correctly.
  • MediaController::destroy() had a dead manual variant cleanup loop that read a non-existent variants column. Removed; the model's deleting hook already handles file + variant cleanup via ImageOptimizer::deleteVariants().
  • Filament edit-media page didn't capture dimensions on non-public disks. Brought to parity with the create page's temp-file download approach.
  • MediaController::store() accepted a caller-supplied disk argument; that's now centralised through cms_media_disk() to keep the disk authority in one place.
  • MediaController::applyFilter() for has_variants queried a non-existent variants column — the filter never matched anything. Now queries the actual has_variants boolean column.
  • MediaResource always returned null for the variants field because it read $this->variants (a non-existent property) instead of meta['variants']. API consumers now get the variant URLs they expect.
  • Replacing a media file in the Filament edit page left stale variant blobs on disk and kept meta['variants'] pointing at the old image, so subsequent getVariantUrl('thumbnail') calls returned the wrong image (or 404'd once anything else cleaned them). The replacement flow now deletes old variants, resets has_variants / optimized_at, and dispatches OptimizeMediaJob to mirror the create flow.
  • config/tallcms.php no longer ships a 'media' block in the standalone scaffold — it was shadowing the package's media.optimization.* sub-tree because Spatie's mergeConfigFrom only merges the first level. The package config defines 'disk' => env('TALLCMS_MEDIA_DISK') directly, so standalone hosts inherit the env-var path through the merge.

Tests

PR #71 added 600+ lines of new coverage spanning MediaController, the disk helper, and the model — including dedicated cases for the has_variants filter (true/false) and resource variants inclusion/omission paths.

Upgrading

No code changes required. To opt into a custom media disk, set TALLCMS_MEDIA_DISK in your .env. Existing installs without the env var continue to use the existing auto-detect path.

Pull request

  • #71 — Add configurable media disk via TALLCMS_MEDIA_DISK (@jakublacko)
v4.4.1

Bug Fixes

Admin resource list queries no longer leak data across tenants (#76)

Filament admin resources for site-owned content were rendering every tenant's records on the index page. Per-record policies still blocked edit/delete, but row titles, slugs, and statuses leaked across tenants — visible to any user with the bare ViewAny permission. This release adds query-level filtering to all site-owned resources via a centralized trait.

Affected resources: CmsPageResource, TallcmsMenuResource, CmsPostResource, CmsCategoryResource, MediaCollectionResource, TallcmsMediaResource, CmsCommentResource, TallcmsContactSubmissionResource. super_admin continues to see everything; standalone (non-multisite) installs are unchanged.

Posts/Categories/Media list query honors user-ownership (#77)

Follow-up to #76. The initial centralization filtered Posts/Categories/Media/MediaCollections by site_id when multisite was active, but the multisite plugin intentionally treats those four models as user-owned with site_id = NULL. Result: a site_owner couldn't see their own freshly-created records. The fix uses user_id for those four resources regardless of multisite mode.

Other

  • AccentColor helper sweep across Stats / How-To / Testimonials / Timeline / Pricing / Features blocks (#72, #73, #74)
  • Render tests for Pro plugin Tabs and Table blocks (#75)
  • Filament-registration plugin wired up with default site-owner role + plan assignment
  • Shield policies for comments, marketplace, redirects, sites
  • Stop rejecting unverified users in canAccessPanel
  • Domain verification feature tests for pro features and site plans

Notes

This release ships behind v4.4.0 in the version sequence — composer.json was bumped to 4.4.0 earlier but never tagged, and the resource-isolation work justified a clean patch number.

v4.4.0

Highlights

Themes can now show their full palette across blocks. Site editors get a per-block Accent Color dropdown on hotspot blocks (Features, Stats, Testimonials, How-To, Timeline, Pricing) — picking secondary, accent, info, success, etc. recolors icons, markers, quote marks, step circles, popular-plan highlights, and timeline node markers instead of every block always rendering primary.

Backwards compatible: every block defaults to `primary`, so existing pages render byte-identical until an editor chooses otherwise.

What's new

`AccentColor` helper class

New `TallCms\Cms\Filament\Blocks\Support\AccentColor` with 9 static methods returning literal Tailwind classes per daisyUI semantic token (primary, secondary, accent, neutral, info, success, warning, error):

  • `text()` / `text20()` — text color, full or `/20` opacity
  • `bg()` / `tint5()` / `tint10()` — backgrounds, full or `/5`/`/10` tint
  • `fill()` — paired `bg-X text-X-content`
  • `border()`, `shadow10()`, `badge()` — border, shadow, daisyUI badge modifier

Tailwind v4's PHP source scanner discovers all variants from the helper's `match()` arms via the existing theme `@source` directive — no theme stylesheet changes needed.

`@accent` Blade directive

`@accent('tint10', $accent_color ?? 'primary')` for inline class attributes, dispatching through `AccentColor::resolve()`.

Three consumption modes documented

PR #72 surfaced an important Blade compiler edge case: the `@accent` directive doesn't work inside Blade component tag attributes (`<x-...>`) because the component-tag compiler captures the class string as a literal before custom directives run. Three patterns:

  1. Plain HTML class: `@accent('tint10', $accent ?? 'primary')`
  2. PHP-built class string inside `@php`: precomputed `$accentTint5 = AccentColor::tint5($accent)` + concat
  3. Blade component tag attribute: inline `{{ \TallCms\Cms\Filament\Blocks\Support\AccentColor::text($accent ?? 'primary') }}`

Form helper

`getAccentColorOptions()` added to the `HasDaisyUIOptions` trait, returning the 8-token semantic map for `Select::make('accent_color')` form fields.

Migrated blocks

Six core blocks pick up the new `accent_color` field:

  • Features — icon container tint + icon color
  • Stats — stat icon + stat value
  • How-To — step number circles
  • Testimonials — quote marks (`/20` opacity) + avatar wrap
  • Timeline — node markers (numbered, icon, date, dot variants) + date label in content card
  • Pricing — popular plan card border, tint, and (Elevate-only) shadow

Standalone overrides at `resources/views/cms/blocks/...` and Elevate theme overrides at `themes/elevate/...` migrated in lockstep so no shadow-view erases the fix.

Intentionally unchanged

  • Elevate features icon container gradient (`bg-gradient-to-br from-primary/10 to-secondary/10`) — multi-color theme decoration; outside `accent_color`'s remit.
  • Elevate testimonials quote marks (`text-gradient-primary` custom utility) — same reason.
  • Posts featured-post badges — block already had its own per-color class maps; not duotone, left alone.

Other fixes in this release

  • AdminPanelProvider guards the registration-plugin bridge with `class_exists()` so fresh CI checkouts (and plugin-mode consumers without the local registration plugin) boot cleanly.

Testing

  • Unit + directive tests for the helper: 158 assertions covering all 9 methods × 8 tokens, the `resolve()` dispatcher, unknown-token / unknown-variant fallbacks.
  • End-to-end render tests in standalone for each migrated block: Features (canonical + Elevate emphasized-card), Stats, How-To, Testimonials, Timeline, Pricing — 35 assertions verifying the right elements recolor with non-primary accents and remain pixel-identical with default primary.

Pull requests

  • #72 — AccentColor helper + Features pilot
  • #73 — Sweep across Stats / Testimonials / How-To / Timeline / Pricing
  • #74 — `badge()` variant (forward-looking; used by tallcms/pro 1.9.0)
  • #75 — Pro plugin Tabs + Table render tests

Pro plugin

`tallcms/pro` 1.9.0 (separately released) ships `accent_color` knobs on Tabs and Table blocks and pins `compatibility.tallcms` to `>=4.4.0`.

Upgrading

No code changes required for existing sites. Editors will see the new Accent Color dropdown on the affected blocks; default value is `primary` so nothing visually changes until they pick a different token.

v4.3.2

Per-site embed code

Embed code (analytics, tracking pixels, chat widgets, custom <head>/body scripts) now scopes per site instead of being installation-wide. The standalone /admin/code-injection page is gone — it lives as an Embed Code tab on the Site edit page (Sites → {site} → Edit → Embed Code).

Why

The previous flow had ambient session-based site resolution that could write to the global tallcms_site_settings table even when the UI implied a specific site was selected. v4.3.2 eliminates that: saves go through SiteSettingsService::setForSite() with the explicit site_id from the edited record. Per-site values write to tallcms_site_setting_overrides and never bleed across sites.

What changed

  • New "Embed Code" tab in the Site edit form: head, body start, body end zones.
  • code_head, code_body_start, code_body_end removed from SiteSetting::$globalOnlyKeys — they now follow the standard per-site override pattern with global fallback.
  • Authorization for embed code follows Site edit permission via SitePolicy. The dedicated Manage:CodeInjection permission is gone from runtime references; the migration that created it is now a no-op for fresh installs.
  • The standalone /admin/code-injection page and its blade view were removed.

Backwards compatibility

  • TallCmsPlugin::make()->withoutCodeInjection() is kept as a deprecated no-op shim so existing panel providers don't fatal on upgrade.
  • Existing global rows in tallcms_site_settings (code_head, etc.) from prior versions are no longer read by the frontend — each site needs its own override now. After upgrading, copy the previous global value into each site that should keep it.
  • Upgraded installs may still have an orphan Manage:CodeInjection permission row in their DB; it's harmless and can be deleted by hand.

Plugin compatibility

The TallCMS Multisite plugin v2.2.0 wires the new embedCodeTab into its own Site edit form. Multisite installs should bump the plugin to v2.2.0 alongside this release.

Docs

Full changelog

See packages/tallcms/cms/CHANGELOG.md.

v4.3.1

Documentation

  • Package UPGRADE.md ships the new "opt-in email verification" recipe — required pre-flight (verified-user backfill + role-less audit), MAIL_MAILER=log footgun, custom-Registered-event warning, and the env switch (REGISTRATION_EMAIL_VERIFICATION=true). Recipe applies to both standalone skeleton installs and plugin-mode hosts.

Standalone skeleton (host-app, not shipped via this package)

The matching wiring landed in the standalone skeleton on main (not in this package release — host-app files don't ship through Composer):

  • App\Models\User implements MustVerifyEmail and gates canAccessPanel() on hasVerifiedEmail() when the env flag is on
  • AdminPanelProvider calls Filament-native ->emailVerification(isRequired: …) and ->emailChangeVerification(…) (note the signature mismatch — see docs/ref-email-verification.md)
  • config/registration.php env fallback so the gate works without the registration plugin installed
  • New docs/ref-email-verification.md — full recipe with custom-signup warning + signature gotcha

Upgrade Notes

  • Backwards-compatible. Default REGISTRATION_EMAIL_VERIFICATION=false — every change is inert until you opt in. Existing installs with admin-created unverified users continue to log in unchanged.
  • Before flipping the env on production, follow the pre-flight in UPGRADE.md: backfill verified-at, audit role-less users, switch MAIL_MAILER from log to a real driver, then php artisan optimize:clear.
v4.3.0

New Features

  • Hierarchical page URLs (#59) — Pages can now resolve via their parent slug chain (e.g. /about/team/sarah). Gated behind the tallcms.pages.hierarchical_urls config flag (default off) so existing installs see no change. URL builders, sitemap, menu resolution, and block link resolution all route through the new path so the flag covers every surface. Includes new feature/unit test coverage.
  • theme:publish-all artisan command — Republishes public/themes/<slug> symlinks for every discovered theme in one shot. Intended for Ploi/Forge/Envoyer post-deploy hooks: each release lands in a fresh directory, so any pre-existing public/themes symlinks are gone — running this restores them without flipping the active theme through the admin.

Maintenance

  • Bump postcss to 8.5.10 (#70)

Upgrade Notes

  • The hierarchical-URL flag is off by default. To opt in, set TALLCMS_HIERARCHICAL_URLS=true (or the equivalent tallcms.pages.hierarchical_urls config value) and clear caches.
  • For zero-downtime deploys, add php artisan theme:publish-all to your post-deploy hook to keep theme assets healthy across releases.
v4.2.1

Patch release fixing a plugin-mode-only regression in the frontend contact form. Drop in via `composer update tallcms/cms`.

Fixed

Contact form broken on plugin-mode frontends (#68)

Plugin-mode installs running v4.0.x – v4.2.0 had a broken contact form on the frontend. Browser console logged:

``` Alpine Expression Error: contactForm is not defined Alpine Expression Error: formError is not defined ```

Two compounding causes:

  1. `@tallcmsCoreJs` resolved to nothing. The Blade directive looked up `resources/js/tallcms-core.js` in the host's Vite manifest. That entry only exists in the standalone scaffold's `vite.config.js`; plugin-mode hosts don't have it. The directive emitted an empty string, and Alpine never registered the `contactForm` / `commentForm` components.

  2. The package-shipped fallback was stale. `packages/tallcms/cms/resources/dist/tallcms.js` had drifted from the source under `resources/js/tallcms/components/`. It missed an `errors:{}` state (so field templates reading `errors.` threw), submitted `_page_url` instead of `_pageUrl` (so submissions rejected with 'Invalid form configuration'), and didn't register `commentForm` at all.

Both fixed:

  • `ThemeManager::getCoreJsTag()` gains a third resolution tier — after dev-hot-file and Vite-manifest checks, it falls back to `public/vendor/tallcms/tallcms.js` (already published by `vendor:publish --tag=tallcms-assets`).
  • The package-shipped `tallcms.js` is rebuilt from the current source. Now contains `Alpine.data('contactForm', ...)`, `Alpine.data('commentForm', ...)`, an `errors:{}` state, and submits `_pageUrl`.

New contract test

`CoreJsRuntimeContractTest` pins the minimum runtime contract: both Alpine components register, errors state initialises, `_pageUrl` is the submission key, and the bundle stays above a sanity-check minimum size. Catches future drift between source and shipped bundle at CI time instead of in production.

Standalone behaviour

Unchanged. The manifest check still fires first whenever the host's Vite build includes the entry. Dev hot-reload still takes absolute precedence.

Upgrading

`composer update tallcms/cms` then redeploy. No manual steps. After the deploy completes, hit any frontend page with a contact-form or comments block — the form should now render and the Alpine console errors are gone.

If you still see the errors after upgrading, confirm `vendor:publish --tag=tallcms-assets --force` is part of your deploy script (the published `public/vendor/tallcms/tallcms.js` is what the new fallback resolves to).

Out of scope (deferred)

  • Package-owned JS build pipeline — tracked in #69. The current bundle rebuild is manual; the proper fix gives the package its own `build:js` script (esbuild) so source-to-bundle drift can't happen again. The contract test from this release acts as a safety net until then.
v4.2.0

Minor release closing the plugin-mode vs standalone parity gap. Plugin-mode installs now get the same theme catalogue, admin rendering, and multisite-aware layouts as standalone — from a single source of truth.

Theme catalogue parity

The tallcms/cms package now ships all eight default themes bundled inside the package at packages/tallcms/cms/resources/themes/:

autumn · blog · corporate · creative · elevate · luxury · minimal · talldaisy

Previously only talldaisy + minimal shipped in the package. Plugin-mode Theme Manager now lists all eight out of the box. Standalone is unchanged for now (its /themes/ source still wins in the dedup) — the standalone-side cleanup lands in a follow-up release once this is verified in production.

Multisite-aware layouts

Every bundled theme's layouts/app.blade.php now renders the site name via SiteSetting::get('site_name', config('app.name')) — navbar, footer, brand labels, logo alt text, and copyright lines. Plugin-mode multisite installs no longer show "Laravel" as a fallback.

For the elevate theme specifically, $siteName and $logo are hoisted to layout scope so the Mega Menu and TallCMS Pro full-header branches get the correct values too.

Admin CSS responsive variants

The package's pre-built tallcms-admin.css now contains md:* / lg:* / sm:* responsive grid and column-span variants used by Filament pages (Theme Manager, Plugin Manager, Site Settings). Previously these were missing from the package bundle and only compiled in via host-side viteTheme(...) in standalone — plugin-mode admin pages rendered cards compressed to one side.

Same fix applied to tallcms-preview.css so block previews in the rich editor get the full responsive class set.

Release pipeline

The release workflow now builds the package + all eight bundled themes in a single step with a single npm install at the package level. Themes resolve their Node deps via NODE_PATH=../../../node_modules.

Upgrading

  • composer update tallcms/cms pulls the new version
  • Run php artisan theme:activate <your-active-theme-slug> after the update — this repoints public/themes/<slug> at the new package location. Until you run it, the active theme's frontend CSS/JS 404s.
  • Do not use tallcms:install --force for this step. That command unconditionally activates talldaisy and would switch you off any other theme.
  • If you've edited any of the default themes (autumn, talldaisy, etc.) in place in /themes/<name>/, copy your customizations to a unique slug before the next skeleton upgrade (e.g. cp -r themes/talldaisy themes/talldaisy-custom + edit theme.json slug + re-activate). Bundled defaults are now the authoritative source; the skeleton's /themes/<name>/ dirs will be stripped in a follow-up release.

Full upgrade guide: UPGRADE.md.

Size impact

The eight bundled themes add ~1 MB to the package (resources/themes/ went from 0.9 MB to 1.9 MB). Acceptable cost for single-source-of-truth theme delivery.

Out of scope (follow-up releases)

  • Strip defaults from standalone skeleton — drop /themes/<name>/ and /public/themes/<name>/ from tallcms/tallcms. Deferred until this release is verified in production on push.sg.
  • Pro / Mega-Menu theme class scanning — plugin-mode users with those plugins installed will need to rebuild the active theme to pick up plugin-specific classes; a php artisan theme:rebuild <slug> command is on the roadmap.

Thanks

Plan-reviewer feedback from this session caught several non-obvious issues before shipping: the off-by-one [@source](https://github.com/source) path math, the missing tallcms.css import path, stale NODE_PATH documentation, dangling public/themes/<slug> symlinks after upgrade, $siteName scope issues in elevate's alternative header branches, and screenshot-reference mismatches. Every one of those would have been a user-facing regression.

v4.1.2

Patch release continuing the plugin-mode install hardening pass. No breaking changes — drop in via `composer update tallcms/cms`.

Fixed

Plugin Manager TypeError after fresh install (#64)

Opening the Plugin Manager page right after `tallcms:install` could crash with:

``` TypeError: Carbon\CarbonImmutable::resolveCarbon(): Argument #1 ($date) must be of type DateTimeInterface|string|null, __PHP_Incomplete_Class given ```

Root cause: stale cache entries from a prior install state deserialized as `__PHP_Incomplete_Class` because the class context wasn't loaded the same way it was at serialization time. `tallcms:install` now runs targeted `cache:clear` + `config:clear` at the end of its pipeline so a fresh install lands with a clean cache. Each clear is wrapped so a misconfigured driver doesn't abort the install.

Improved

Plugin install docs are now linear (#65)

The plugin installation walkthrough used to end at Step 6 (asset build) and defer frontend route setup to a top-level "Frontend Routes (Plugin Mode)" section ~100 lines later, after the unrelated standalone walkthrough. Plugin-mode users finishing `tallcms:install` had to scroll past standalone-only content to find the env var, the welcome-route conflict resolution, and the homepage step.

The frontend-routes content now lives in the linear plugin install flow as Steps 7-9:

  • Step 7 — publish TallCMS frontend assets
  • Step 8 — enable frontend routes (env var + welcome-route conflict + Alpine.js note)
  • Step 9 — mark a Page as Homepage in the admin

A short transition between Steps 6 and 7 tells admin-only adopters where they can stop.

Out of scope (deferred)

  • Auto-edit installer ergonomics for HasRoles + plugin registration + welcome-route removal. Same-process re-check is unreliable because the User class and Filament panels are already loaded at installer runtime; needs its own design with an "edit then exit, ask user to rerun" flow. Will land together so the auto-edit logic only needs designing once.
  • Laravel 13 support. Still upstream-blocked on `lazychaser/laravel-nestedset` — see #61.
v4.1.1

Patch release fixing two plugin-mode install friction points and tightening the install-time UX. No breaking changes — drop in via `composer update tallcms/cms`.

Fixed

Plugin-mode page editor 500 (#63)

Editing a Page or Post in plugin mode would crash with `Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest: resources/css/filament/admin/preview.css`. That entry only exists in standalone TallCMS's `vite.config.js`; plugin-mode hosts don't have it.

The fix ships a pre-built `tallcms-preview.css` (~255 KB, daisyUI block-preview scoping) in the package's `resources/dist/` and registers it through Filament's asset system in plugin mode. Standalone continues to use its existing `@vite` path so dev hot-reload while iterating on `preview.css` keeps working — both modes are gated so you never get a double-load.

A new `ViteManifest::hasEntry()` helper guards the `@vite` call in the rich-editor view, with full Blade-level regression coverage (manifest absent → guard skips; manifest present → guard runs).

Install-time warning when host Vite manifest is missing (#62)

Plugin-mode adopters running `php artisan tallcms:install` would hit `ViteManifestNotFoundException` the moment they opened the admin, because the host app's Vite manifest hadn't been built. Generic Filament behavior, but the timing made it look like TallCMS was broken.

`tallcms:install` now checks for `public/build/manifest.json` at the end of its run and emits a clear warning with the exact `npm install && npm run build` commands when missing. Silent when the manifest exists.

Docs

  • `docs/gs-installation.md` — new Step 6 covering frontend asset build, plus two Common Pitfalls entries for the Vite errors above
  • Package README — adds the Node 20+ requirement that was previously only in the root README

Out of scope (deferred)

  • Auto-edit installer ergonomics for HasRoles + plugin registration. Same-process re-check is unreliable because the User class and Filament panels are already loaded by the time the installer wants to verify; needs its own design with an "edit then exit, ask user to rerun" flow.
  • Laravel 13 support. Still upstream-blocked on `lazychaser/laravel-nestedset` — see #61.
v4.1.0

First minor bump in the 4.x line — new configurable admin API, six seed templates, a new theme, and two contributor bug fixes.

Highlights

Configurable resource labels (new API)

Rename any of the 10 admin resources — Pages, Posts, Categories, Menus, Media, Media Collections, Comments, Contact Submissions, Users, Site Settings — without touching package files. Two override paths:

  • Config: tallcms.labels.<key>.<facet> in config/tallcms.php
  • Fluent: TallCmsPlugin::make()->postLabel('Articles', 'Article')

Omitted singular/navigation arguments preserve the configured defaults, so ->mediaLabel('Assets') keeps the "Media Library" navigation label intact. Global search result metadata also reflects the rename. Shield permissions are unaffected (they derive from model class names, not labels).

Six new seed templates

  • tallcms:seed-keystone-template — realtor / personal agent site
  • tallcms:seed-ink-template — blog
  • tallcms:seed-launchpad-template — property launch landing page
  • tallcms:seed-spotlight-template — premium property launch with in-hero lead capture
  • tallcms:seed-counsel-template — law firm site, conservative tone
  • tallcms:seed-pushsg-template — meta template for the PUSH.SG marketing site itself

New theme: Luxury

Ships alongside the existing themes.

Bug fixes

  • Menu item tree labels now respect the admin's active locale (previously stuck on the app default after switching)
  • Contact form signature normalization now mirrors both TrimStrings and ConvertEmptyStringsToNull middleware, fixing "Invalid form configuration" errors on sites with whitespace or empty-string block defaults
  • Form field names like unit_type (underscores) now validate correctly in Hero and Contact Form blocks

Other

  • MakeTheme scaffold now uses SiteSetting::get('site_name') with a config fallback, so scaffolded themes pick up the per-site name
  • Sites admin resource surfaces the owner name + email column for super admins

Thanks

Big thanks to [@jakublacko](https://github.com/jakublacko) for two first-time contributions that shipped in this release:

  • #56 — fixes the menu-item tree not respecting the active locale
  • #57 — the configurable resource labels feature

Both clean diagnoses and well-targeted fixes. More contributions welcome.

v4.0.24

Enhancement

  • Theme Manager now has a site switcher at the top of the page. Super_admins see every active site in the install; site_owners see the sites they own. The switcher is only rendered when the user has more than one site to choose from, keeping the single-site flow uncluttered. Combined with v4.0.23's theme-write scoping fix, super_admins can now pick "I want to configure portal.test's theme" and activate without affecting any other site on the install.

Implementation

  • `manageableSites()` — exposes `[site_id => "Name (domain)"]` filtered by role.
  • `switchSite($siteId)` — sets the admin session and redirects to refresh the page context. Validates that the user can actually manage the chosen site; a site_owner can't point their session at another owner's site even if they know the id.

Test Coverage

  • `ThemeManagerSiteOwnerScopingTest` extended to 8 tests: resolver matrix + switcher filter + unauthorized-site rejection.
v4.0.23

Bug Fix

  • Theme activation and preset changes now scope to the site_owner's own site instead of writing to the installation-wide `config/theme.php`. Previously, when a SaaS site_owner activated a theme from the Theme Manager, the change wrote to the global config file, flipping every other site on the install to the same theme.

Root Cause

`ThemeManager::getMultisiteContext()` read `session('multisite_admin_site_id')` and returned null when the session value wasn't set. A site_owner with exactly one owned site never touches the Site Switcher (nothing to switch), so the session value stayed null, and `activateTheme()` took the "no context" branch — which writes the global config.

Fix

When no session site is selected:

  • site_owner (non-super-admin) → falls back to their first owned active site (same pattern MenuOverviewWidget already uses).
  • super_admin → returns null, preserving the global-defaults management path for installation-level admins.

Explicit Site Switcher selections still win over the fallback. "All Sites" sentinel still returns null.

Test Coverage

  • `ThemeManagerSiteOwnerScopingTest` — 5 tests covering the resolver matrix: site_owner fallback, super_admin globality, session precedence, "All Sites" sentinel, zero-owned-sites case.
v4.0.22

Bug Fix

  • Contact-form submissions no longer fail with "Invalid form configuration" when any config string has leading/trailing whitespace. Laravel's `TrimStrings` middleware strips whitespace from every string in the request body, including nested values inside `_config`. Render-time `signConfig()` hashed the untrimmed config, but by the time the controller verified the submission, the whitespace was gone — HMAC mismatch, every submission rejected.

    Surfaced specifically in v4.0.20 installs where a site owner added an Auto-Reply Message ending in a trailing space or newline. Pre-existing configs with incidental whitespace would have hit the same bug; the small default-fields config just didn't trigger it.

Fix

`signConfig` recursively trims strings before hashing. Both render-time and verify-time signing apply the same normalization, so the HMACs agree regardless of whitespace Laravel strips.

Internal whitespace (spaces/newlines inside the text) is preserved — only leading/trailing whitespace is trimmed, matching TrimStrings behavior.

Also

Removed the v4.0.21 diagnostic logging that helped identify the cause.

Test Coverage

  • `ContactFormSignatureTrimmingTest` — 3 tests: trimmed/untrimmed match, different content still differs, internal whitespace preserved.
v4.0.21

Temporary diagnostic release. Adds detailed logging when ContactFormController rejects a form submission with "Invalid form configuration", capturing the submitted and computed HMAC signatures, the signed payload, and config key order. Helps pinpoint serialization mismatches. Will be replaced by a proper fix in the next release.

v4.0.20

Enhancement (closes #53)

  • Contact Form blocks can now carry their own auto-reply copy. Until now, the auto-reply body paragraph was baked into the Blade template — only the site name was dynamic. The Contact Form block configurator now exposes an Auto-Reply Message textarea in the Form Settings section. Each form on a site can have its own reply text (release-window language, listing-specific wording, compliance disclaimers, locale matches, etc.). Leave blank to use the default paragraph.

Flow

block config -> page content JSON -> signed config in block view -> submitted with form -> controller verifies + pulls `auto_reply_message` -> passed to `ContactFormAutoReply` -> rendered in the auto-reply email.

The greeting, field-summary, site-name branding, and disclaimer footer stay in the template — this is main-paragraph customization, not a full template override. Content is HTML-escaped and newlines become `` so user copy can span paragraphs without injecting markup.

Test Coverage

  • `ContactFormCustomAutoReplyMessageTest` — 3 tests: custom replaces default, blank falls back to default, HTML escaping holds.
v4.0.19

Bug Fix

  • Auto-reply emails now brand with the submission's site name. Previously `ContactFormAutoReply` used `config('app.name')` (the install-wide `APP_NAME`) for the subject and body greeting. On SaaS, a visitor who submitted a form on `portal.test` received `Thank you for contacting TallCMS SaaS` — leaking the install's meta-brand and breaking the illusion that each site is its own property.

The mailable now resolves the site name from `submission.site_id` (`tallcms_sites.name`) and passes it to the auto-reply view. Falls back to `config('app.name')` when the submission has no `site_id` (single-site installs), so non-multisite behavior is unchanged.

Example on a site named "Portal":

  • Before: `Thank you for contacting TallCMS SaaS`
  • After: `Thank you for contacting Portal`

Test Coverage

  • `ContactFormAutoReplyUsesSiteNameTest` — 2 tests covering the site-name path and the fallback.
v4.0.18

Bug Fix

  • Contact-form admin notifications now route to the site owner by default instead of inheriting a potentially irrelevant global contact email. Previously, a site cloned from a template (with no `contact_email` set in Site Settings) would fall back to the install-wide global — which on SaaS installs was often a placeholder like `noreply@saas.test`. Site owners never received their own form submissions.

New resolution chain

In order of precedence:

  1. Site-specific `contact_email` override (site owner set it in Site Settings).
  2. Site owner's user email (the address they registered with).
  3. Global `contact_email` setting.
  4. Skip the send.

Exposed as

`ContactFormController::resolveAdminRecipient(): ?string` — public static, so other code paths (e.g., the Livewire contact form in the standalone app) can use the same logic without duplication.

Test Coverage

  • `ContactFormAdminRecipientResolutionTest` — 4 tests locking the resolution chain: override wins, owner email wins over global, global fallback, null when nothing is configured.
v4.0.17

Bug Fixes

  • Comments and Contact Submissions lists now scope to the auth user's owned sites. Filament policies gate record-level access but don't filter list queries, so a `site_owner` viewing `/admin/cms-comments` or `/admin/tallcms-contact-submissions` saw every tenant's records. Horizontal information disclosure. Fixed with a new `ScopesQueryToOwnedSites` trait applied to both resources. super_admin bypasses the scope; single-site installs short-circuit before filtering.

  • Navigation badges (pending-comment count, unread-submission count) also respect the ownership filter — previously the badges showed counts from other tenants' sites.

New

  • `site_owner` role now has `View:ThemeManager` so a SaaS user can manage their site's theme and preset. Added to both `ShieldSeeder` (fresh installs) and `tallcms:shield-sync-site-owner` (existing installs). Running `tallcms:update` auto-syncs the new permission alongside the role.

Pair With

Multisite plugin v2.1.3 for the template-clone PII fix (different repo, same logical release). https://github.com/tallcms/multisite-plugin/releases/tag/v2.1.3

Test Coverage

  • `ResourceQueryScopingToOwnedSitesTest` — 4 tests covering own-site visible, other-site hidden, super-admin sees all, user-owning-no-sites sees nothing.
  • `SiteCloneDoesNotLeakPiiTest` — 3 tests locking the PII blocklist contract (in the multisite plugin repo).
v4.0.16

Bug Fix

  • Page slug uniqueness now scoped to the record's own site. Cloning a template site and then editing the cloned homepage would fail with "This slug is already used by another item in en." — even though the clone was on a different site from the template's original. `UniqueTranslatableSlug` resolved its scoping `site_id` via the admin session and multisite resolver, but neither works for a `site_owner` who hasn't touched the Site Switcher (session empty, resolver doesn't auto-bind to owned sites in admin context). The check fell back to a global uniqueness query, finding the template's original `home` slug on the template-source site.

The rule now takes an explicit `siteId` constructor parameter. The page form passes `$record?->site_id` for edits and `$livewire->ownerSiteId` for creates — authoritative, bypassing the session/resolver fallback. Existing per-site scoping semantics preserved when no explicit id is supplied.

Repro

  1. Super admin creates Site A with a "home" page.
  2. Enable Site A as a template source.
  3. site_owner clones Site A → Site B.
  4. site_owner opens the cloned homepage on Site B, edits anything, clicks Save.

Pre-fix: validation error "This slug is already used by another item in en." Post-fix: save succeeds; both sites have their own independent "home" slug.

Test Coverage

  • `UniqueTranslatableSlugSiteScopingTest` — 3 tests covering cross-site allow, same-site reject, and ignore-id-excludes-self.
v4.0.15

Bug Fix / UX

  • `tallcms:update` now auto-syncs Shield roles introduced in later releases. v4.0.14 added the `site_owner` role, but existing installs that ran `tallcms:update` to v4.0.14 got the code without the role — because `tallcms:update` doesn't re-run seeders. Users either had to know to run `php artisan tallcms:shield-sync-site-owner` manually or hit a missing-role error when installing the Registration plugin v1.1.0 (which now defaults new signups to `site_owner`).

The updater now calls `tallcms:shield-sync-site-owner` during its cache-clearing step. The sync command is idempotent, so running it on every update is safe — it only does work when a role or permission is actually missing. Future role additions can be plugged into the same foreach loop with one line.

Effect on Existing Installs

Running `tallcms:update --skip-db-backup` now:

  • Applies the update as usual.
  • Republishes Filament assets.
  • Creates the `site_owner` role if it doesn't exist (no-op if it does).

No manual command required.

v4.0.14

New Feature

  • `site_owner` role — the SaaS-flow role for users who own a site and manage its content end-to-end (pages, posts, menus, categories, media, comments, contact submissions, template gallery). Purpose-built for the site-builder SaaS model; the legacy `author` role (blog-post writer + submit for review) was a poor fit for that workflow.

How It Works

Three pieces:

  1. `ShieldSeeder` now defines the `site_owner` role with 47 permissions on fresh installs.
  2. `tallcms:shield-sync-site-owner` command creates/updates the role idempotently on existing installs — safe to run repeatedly, creates any missing permissions, doesn't touch other roles or user-role assignments.
  3. `CmsCommentPolicy` and `TallcmsContactSubmissionPolicy` now layer a site-ownership check on top of Shield's role/permission check (same `ChecksSiteOwnership` trait already used by `CmsPagePolicy` and `TallcmsMenuPolicy`). Without this, a `site_owner` in multisite would see every tenant's comments and submissions — horizontal privilege escalation. Super-admins still see everything; single-site installs bypass the check transparently.

Usage on Existing Installs

Run once to create the role:

```bash php artisan tallcms:shield-sync-site-owner ```

Dry-run first to preview:

```bash php artisan tallcms:shield-sync-site-owner --dry-run ```

Then assign to users via Filament Shield's role manager or tinker:

```php $user->syncRoles(['site_owner']); ```

Pair with the Registration plugin ≥1.1.0 to assign site_owner automatically to every new signup.

Test Coverage

  • `SiteOwnerCommentSubmissionPolicyTest` — 6 tests: own-site positive, other-site negative, super-admin bypass, orphan-record guard.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
emuniq/filament-browser-notifications
syriable/filament-translator
hungnm28/livewire-form
wenprise/eloquent
crudly/encrypted
fadion/bouncy
cuci/prototurk-sdk
gos/pubsub-router-bundle
cuci/prototurk-sdk-symfony
clementtalleu/easyadmin-markdown-bundle
codeflextech/permission-manager
karnoweb/livewire-datepicker
sayedenam/sayed-dashboard
milito/query-filter
apiboxsym/user-bundle
apiboxsym/health-check-bundle
jayeshmepani/jpl-moshier-ephemeris-php
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui