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.
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.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)MenuCache helper that uses tagged cache when the store supports it, with a versioned-key fallback for non-tag stores. (#96)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)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.
ImageColumn missing disk() in CmsPages and CmsPosts tables, with regression tests. (#93)ref-rich-editor.mdNo 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.
Thanks to the following contributors whose work shipped in this release:
ImageColumn disk fix (#93)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.
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".
BlockChromePlugin (a Filament RichContentPlugin). Three ProseMirror plugins inside it: BlockChromeView, OutlineSyncView, SlashCommandView. Bundle (~198KB) is loaded on-request — pages without an editor pay nothing.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.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.FilamentAsset).:title tooltip provides a fallback for nowNo breaking changes. Existing CmsRichEditor usage continues to work unchanged. Custom block packages are unaffected.
Block grid normalization and per-site theme UI control toggles.
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.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.bg-white / text-gray-500 colors in Parallax CTA and Document List with daisyUI tokens so they respect dark and branded themes.aria-labels to the close / prev / next icon-only buttons in the Image Gallery lightbox.theme.json supports.theme_controller, supports.search, supports.language_switcher).true so existing installs keep current rendering on upgrade.make:theme gains prompts and --include-search / --include-language-switcher flags to scaffold matching component stubs and theme.json supports flags.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.
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.
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.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.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.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.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.
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.
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.[@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."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.
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.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.
| 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:
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.
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:
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.
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.
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.
`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) ```
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:
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.
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) ```
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.
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.
DashboardSitePicker widgetNew 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.
Every selection (mount + updatedSelected) is validated against the current user's access via resolveAuthorizedSiteValue():
__all_sites__ is only valid for super_admins.__all_sites__ left over from a previous super_admin login on a shared browser) are normalized on mount.HasMultisiteWidgetContext traitNew 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.
MenuOverviewWidget and ContentHealthWidget now adopt the trait and add #[On('dashboard.site-changed')] listeners so they re-render when the picker fires.
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.
| 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.
getMultisiteSiteId (specific id, all-sites sentinel, super_admin default-site fallback, regular-user first-owned fallback, QueryException safety net), getMultisiteName, and isAllSitesSelected.__all_sites__ / inactive site, mount normalizes stale foreign-user session value, mount rejects stale __all_sites__ for non-super-admin).setPeriod/refreshData short-circuiting on All Sites, event-driven data clearing, License heading and sentinel status.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.
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.
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.
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.
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.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.
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().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.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.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.
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.
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.
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.
canAccessPanelThis 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.
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.
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):
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('tint10', $accent_color ?? 'primary')` for inline class attributes, dispatching through `AccentColor::resolve()`.
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:
`getAccentColorOptions()` added to the `HasDaisyUIOptions` trait, returning the 8-token semantic map for `Select::make('accent_color')` form fields.
Six core blocks pick up the new `accent_color` field:
Standalone overrides at `resources/views/cms/blocks/...` and Elevate theme overrides at `themes/elevate/...` migrated in lockstep so no shadow-view erases the fix.
`tallcms/pro` 1.9.0 (separately released) ships `accent_color` knobs on Tabs and Table blocks and pins `compatibility.tallcms` to `>=4.4.0`.
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.
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).
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.
code_head, code_body_start, code_body_end removed from SiteSetting::$globalOnlyKeys — they now follow the standard per-site override pattern with global fallback.SitePolicy. The dedicated Manage:CodeInjection permission is gone from runtime references; the migration that created it is now a no-op for fresh installs./admin/code-injection page and its blade view were removed.TallCmsPlugin::make()->withoutCodeInjection() is kept as a deprecated no-op shim so existing panel providers don't fatal on upgrade.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.Manage:CodeInjection permission row in their DB; it's harmless and can be deleted by hand.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.
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.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 onAdminPanelProvider 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 installeddocs/ref-email-verification.md — full recipe with custom-signup warning + signature gotchaREGISTRATION_EMAIL_VERIFICATION=false — every change is inert until you opt in. Existing installs with admin-created unverified users continue to log in unchanged.UPGRADE.md: backfill verified-at, audit role-less users, switch MAIL_MAILER from log to a real driver, then php artisan optimize:clear./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.TALLCMS_HIERARCHICAL_URLS=true (or the equivalent tallcms.pages.hierarchical_urls config value) and clear caches.php artisan theme:publish-all to your post-deploy hook to keep theme assets healthy across releases.Patch release fixing a plugin-mode-only regression in the frontend contact form. Drop in via `composer update tallcms/cms`.
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:
`@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.
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:
`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.
Unchanged. The manifest check still fires first whenever the host's Vite build includes the entry. Dev hot-reload still takes absolute precedence.
`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).
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.
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.
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.
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.
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.
composer update tallcms/cms pulls the new versionphp 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.tallcms:install --force for this step. That command unconditionally activates talldaisy and would switch you off any other theme./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.
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.
/themes/<name>/ and /public/themes/<name>/ from tallcms/tallcms. Deferred until this release is verified in production on push.sg.php artisan theme:rebuild <slug> command is on the roadmap.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.
Patch release continuing the plugin-mode install hardening pass. No breaking changes — drop in via `composer update tallcms/cms`.
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.
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:
A short transition between Steps 6 and 7 tells admin-only adopters where they can stop.
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`.
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).
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.
First minor bump in the 4.x line — new configurable admin API, six seed templates, a new theme, and two contributor bug fixes.
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:
tallcms.labels.<key>.<facet> in config/tallcms.phpTallCmsPlugin::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).
tallcms:seed-keystone-template — realtor / personal agent sitetallcms:seed-ink-template — blogtallcms:seed-launchpad-template — property launch landing pagetallcms:seed-spotlight-template — premium property launch with in-hero lead capturetallcms:seed-counsel-template — law firm site, conservative tonetallcms:seed-pushsg-template — meta template for the PUSH.SG marketing site itselfShips alongside the existing themes.
TrimStrings and ConvertEmptyStringsToNull middleware, fixing "Invalid form configuration" errors on sites with whitespace or empty-string block defaultsunit_type (underscores) now validate correctly in Hero and Contact Form blocksMakeTheme scaffold now uses SiteSetting::get('site_name') with a config fallback, so scaffolded themes pick up the per-site nameBig thanks to [@jakublacko](https://github.com/jakublacko) for two first-time contributions that shipped in this release:
Both clean diagnoses and well-targeted fixes. More contributions welcome.
`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.
When no session site is selected:
Explicit Site Switcher selections still win over the fallback. "All Sites" sentinel still returns null.
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.
`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.
Removed the v4.0.21 diagnostic logging that helped identify the cause.
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.
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.
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":
In order of precedence:
`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.
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.
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
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.
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.
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.
Running `tallcms:update --skip-db-backup` now:
No manual command required.
Three pieces:
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.
How can I help you explore Laravel packages today?