Upgrading to Evolve v2.0
Evolve v2.0 is a major release that combines three changes:
- Monorepo: All 11 packages consolidated into a single
racerfish/evolvepackage - Livewire 3: Full migration from Livewire 2 to 3
- Laravel 12: Framework upgrade from Laravel 10 to 12
What Changed
| Before (v1.x) | After (v2.0) |
|---|---|
| 11 separate Composer packages | 1 unified racerfish/evolve package |
Livewire 2 (wire:model.defer, $this->emit()) | Livewire 3 (wire:model, $this->dispatch()) |
| Laravel 10 | Laravel 12 |
Carbon 2 (formatLocalized()) | Carbon 3 (isoFormat()) |
| Multiple VCS repositories | 1 VCS repository (racerfish/evolve-cms) |
Read the pitfalls section first
Every step below is necessary but not sufficient. The failures that actually cost time on client sites are collected in Pitfalls Found in Real Upgrades: a dead backend from stale Livewire 2 assets, a deploy aborted by view:cache, two Alpine instances, an unresolvable image driver. They all survive a clean composer update and surface later. Skim that section before you start and work through it before you call the upgrade done.
Laravel 12 is required
All of Evolve v2.x requires Laravel 12. Skip Laravel 11 during the upgrade. If you land on Laravel 11, Composer will silently resolve racerfish/evolve:^2.0 to 2.0.2 because 2.1.0+ pulls in laravel/ai ^0.4 which requires Laravel 12. The v2.0.x line also has a known UI bug (an Alpine 2 vs. Livewire 3 @entangle version skew) that breaks search inputs and resource filters in the backend. Going directly to Laravel 12 avoids both issues.
If your composer update stops at 2.0.x, confirm with composer why-not racerfish/evolve 2.1.1. A laravel/framework entry in the output means the framework upgrade is still pending.
Prerequisites
- PHP 8.3 or higher
- MySQL 8.0+ or PostgreSQL 13+
- Back up your database before upgrading
- Ensure all pending migrations have been run:
php artisan migrate:status - Review any published Livewire components in
app/Livewire/orapp/Http/Livewire/for LW2 patterns
Step 1: Update Composer Repositories
Replace any individual VCS repositories in your composer.json with the single monorepo:
"repositories": [
{
"type": "vcs",
"url": "git@github.com:racerfish/evolve-cms.git"
}
]Remove all old repository entries pointing to racerfish/evolve-core, racerfish/evolve-blog, racerfish/evolve-theme, etc.
Step 2: Swap Packages
Remove all individual packages and require the unified package:
composer remove \
racerfish/blog \
racerfish/company \
racerfish/devtools \
racerfish/event \
racerfish/faq \
racerfish/locator \
racerfish/project \
racerfish/scout24 \
racerfish/shopify \
racerfish/theme
composer require racerfish/evolve:^2.0TIP
You only need to remove the packages that your instance currently has installed. If you don't use racerfish/scout24, skip removing it.
INFO
The ^2.0 caret constraint resolves to >=2.0.0 <3.0.0, so future minor releases (2.1, 2.2, etc.) and patch updates are picked up automatically when you run composer update racerfish/evolve. The version is not pinned to 2.0.
Step 3: Update Laravel Framework
Update your composer.json to require Laravel 12 and its compatible dependencies:
"require": {
"php": "^8.3.2",
"laravel/framework": "^12.0",
"racerfish/evolve": "^2.0"
}Also update any third-party packages that don't support Laravel 12. Common ones:
| Package | v1.x (L10) | v2.x (L12) |
|---|---|---|
sentry/sentry-laravel | ^3.4 | ^4.0 |
nunomaduro/collision | ^7.0 | ^8.0 |
larastan/larastan | nunomaduro/larastan ^2.2 | ^3.0 |
spatie/laravel-ignition | ^2.0 | ^2.0 (compatible) |
For example, if your project uses Sentry:
"require": {
"sentry/sentry-laravel": "^4.0"
}And in require-dev:
"require-dev": {
"nunomaduro/collision": "^8.0",
"larastan/larastan": "^3.0"
}WARNING
Run composer why-not laravel/framework 12.0 to identify any packages blocking the upgrade. The error message will show which package constrains illuminate/support to L11 or earlier. Apply the same check for composer why-not racerfish/evolve 2.1.1 to confirm the framework is the only blocker.
Step 4: Remove the Livewire Assets Publish Script
In your composer.json, remove the livewire:assets publish line from post-autoload-dump. Livewire 3 handles assets automatically.
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
]
}Removing the line is not enough
It stops the directory being recreated. It does not delete what is already there, and a leftover public/vendor/livewire/ silently kills every wire:click in the backend. Delete it now:
rm -rf public/vendor/livewireStep 5: Update dont-discover
Simplify the dont-discover list. The monorepo only needs one entry:
"extra": {
"laravel": {
"dont-discover": [
"racerfish/evolve"
]
}
}Step 6: Add Livewire 3 Configuration
Add legacy_model_binding to config/livewire.php:
// config/livewire.php
return [
'legacy_model_binding' => true,
'temporary_file_upload' => [
'rules' => 'file|max:50000',
],
];WARNING
The legacy_model_binding setting is required for Evolve's wire:model="item.property" pattern to work in Livewire 3. Do not remove this setting.
This file replaces whole arrays
Livewire merges your config over its own with a shallow array_merge, so the partial temporary_file_upload block above deletes the keys it does not repeat and leaves uploads misconfigured. Copy the full block from config/livewire.php overrides whole arrays, not single keys.
Step 7: Migrate Frontend Template (Livewire Directives)
Livewire 3 removed the @livewireStyles and @livewireScripts Blade directives. If your theme layout uses these, you will get:
Call to undefined method Livewire\LivewireManager::scripts()Fix for frontend layouts
In your theme's main layout (typically theme/views/frontend.blade.php or similar), replace the old Blade directives with direct calls to the FrontendAssets class:
{{-- Before (LW2) --}}
<head>
...
@livewireStyles
</head>
<body>
...
@livewireScripts
</body>
{{-- After (LW3) --}}
<head>
...
{!! \Livewire\Mechanisms\FrontendAssets\FrontendAssets::styles() !!}
</head>
<body>
...
{!! \Livewire\Mechanisms\FrontendAssets\FrontendAssets::scripts() !!}
</body>WARNING
Do NOT use @livewireScriptConfig as a replacement. That directive only outputs the config JSON and suppresses auto-injection of the actual livewire.js script, which means Alpine.js (bundled with Livewire 3) will never load. Use FrontendAssets::scripts() which includes both the JS file and the config.
TIP
For frontend theme layouts that only occasionally render Livewire components (e.g., contact forms), you can alternatively rely on auto-injection: remove all Livewire directives entirely, and Livewire 3 will auto-inject its assets into any response with </html> that renders a Livewire component. However, explicit inclusion via FrontendAssets is more reliable.
Sites with Cookiebot / cookie consent managers
If your theme previously used the cookieconsent pattern to prevent consent managers from blocking Livewire:
{{-- Before (LW2) --}}
{!! str_replace('<script', '<script data-cookieconsent="ignore"', \Livewire\Livewire::scripts()) !!}This no longer works because Livewire::scripts() was removed from the facade. In LW3, use the static method on the FrontendAssets class and disable auto-injection:
// config/livewire.php
return [
'inject_assets' => false, // disable auto-injection so we control the output
'legacy_model_binding' => true,
// ...
];{{-- After (LW3) --}}
{!! str_replace('<script', '<script data-cookieconsent="ignore"', \Livewire\Mechanisms\FrontendAssets\FrontendAssets::styles()) !!}
{!! str_replace('<script', '<script data-cookieconsent="ignore"', \Livewire\Mechanisms\FrontendAssets\FrontendAssets::scripts()) !!}Place styles in <head> and scripts before </body> (same positions as before). When inject_assets is false, you are fully responsible for including both styles and scripts.
WARNING
When using inject_assets => false, the @livewireScriptConfig directive is not needed. The FrontendAssets::scripts() call already includes the config. Do not use both.
Published backend views
If you have published evolve backend views (in theme/views/vendor/evolve/), check for @livewireStyles and @livewireScripts in app.blade.php and blank.blade.php and apply the same replacement to @livewireScriptConfig.
Step 8: Publish the Module Configuration
Add the modules array to your config/evolve.php to control which modules are loaded:
// config/evolve.php
return [
'modules' => [
'blog' => true,
'company' => true,
'devtools' => true,
'event' => true,
'faq' => true,
'locator' => true,
'project' => true,
'scout24' => true,
'shopify' => true,
],
// ... rest of your existing config
];Set any unused modules to false to prevent loading their routes, migrations, and views.
Step 9: Migrate Published Livewire Components
If you have published Livewire components in app/Http/Livewire/ or app/Livewire/, update them for Livewire 3:
Namespace change
// Before
namespace App\Http\Livewire\Blocks\Blog;
// After
namespace App\Livewire\Blocks\Blog;Move the files from app/Http/Livewire/ to app/Livewire/ accordingly.
Wire model syntax
{{-- Before (LW2) --}}
<input wire:model.defer="search" />
{{-- After (LW3) --}}
<input wire:model="search" />In Livewire 3, wire:model is deferred by default. Use wire:model.live for instant reactivity.
Event dispatch
// Before (LW2)
$this->emit('eventName', $data);
$this->emitUp('eventName', $data);
// After (LW3)
$this->dispatch('eventName', data: $data);All dispatch calls must use named parameters.
Form submit
{{-- Before (LW2) --}}
<form wire:submit.prevent="save">
{{-- After (LW3) --}}
<form wire:submit="save">Computed properties
// Before (LW2)
public function getItemsProperty() { ... }
// After (LW3)
use Livewire\Attributes\Computed;
#[Computed]
public function items() { ... }Query string
// Before (LW2)
protected $queryString = ['search' => ['except' => '']];
// After (LW3)
use Livewire\Attributes\Url;
#[Url(except: '')]
public string $search = '';Entangle
{{-- Before (LW2) --}}
query: @entangle('search').defer,
{{-- After (LW3) --}}
query: @entangle('search'),In Livewire 3, @entangle is deferred by default. Use @entangle('search').live for instant sync.
Step 10: Update Date Formatting (Carbon 3)
Laravel 12 ships with Carbon 3, which removed formatLocalized(). Replace with isoFormat():
// Before
$date->formatLocalized('%d. %B %Y');
// After
$date->isoFormat('DD. MMMM YYYY');Common format conversions:
| strftime | ICU (isoFormat) |
|---|---|
%d | DD |
%m | MM |
%Y | YYYY |
%B | MMMM |
%A | dddd |
%H:%M | HH:mm |
Step 11: Clean Up Published Migrations
If you previously published package migrations to database/migrations/, remove the copies. The monorepo loads migrations directly from the package.
Check for files like create_blog_*, create_shopify_*, create_faq_* in your migrations directory and delete them if they are copies of package migrations.
Step 12: Update Envoy Import Path
If you use Laravel Envoy for server tasks, update the import path in your Envoy.blade.php to reflect the monorepo structure:
{{-- Before (v1.x) --}}
@import('vendor/racerfish/evolve/bin/Envoy.blade.php')
{{-- After (v2.0) --}}
@import('vendor/racerfish/evolve/core/bin/Envoy.blade.php')The Envoy tasks file moved into the core/ subdirectory as part of the monorepo consolidation.
Step 13: Clear Caches and Run Migrations
composer install
php artisan optimize:clear
php artisan migrateStep 14: Rebuild Assets
yarn backend:install
yarn backend:build
yarn theme:install
yarn theme:buildStep 15: Verify
# Check the app boots
php artisan about
# Verify routes load
php artisan route:list
# Check migration status
php artisan migrate:status
# Run static analysis
php artisan analyseVisit your backend panel and verify:
- Login works
- Dashboard loads
- Page editor functions (block CRUD, media picker)
- Navigation manager works
- Forms and leads display correctly
- Pagination and search respond. A dead panel here means the stale Livewire 2 assets, see Delete the Livewire 2 published assets.
- An image renders through the
/imgroute. A failure here is the Intervention Image 3 driver.
Then work through Pitfalls Found in Real Upgrades. Several of those failures cannot be seen from the console at all.
Staying on v1.x (Legacy)
If a client site cannot be upgraded to Livewire 3 (e.g., custom frontend components that depend on LW2 patterns), you can stay on the legacy version:
"repositories": [
{
"type": "vcs",
"url": "git@github.com:racerfish/evolve-cms.git"
}
]composer require racerfish/evolve:v1.99The v1.99 tag points to the last pre-Livewire 3 release with the monorepo structure. This version receives critical bug fixes only.
Updating After the Migration
Future v2.x updates (2.1, 2.2, etc.) are a single command:
composer update racerfish/evolve
php artisan migrate
php artisan optimize:clear && php artisan optimize # the deploy runs this, catch failures locallyBecause the composer constraint is ^2.0, the command will bring in any new 2.x minor or patch release automatically. A major release (3.x) will require an explicit constraint bump and its own upgrade guide.
Upgrading within 2.x
Upgrading within v2.x is composer update racerfish/evolve. The releases below need more than that. Newest first; a project jumping several minors works through every section it crosses, oldest first.
To 2.22
Switch the email module on where newsletters or Brevo are used. behaviour change The
emailmodule (campaigns, Brevo contact sync, the Email entry in the sidebar) is off by default now. An install that never set the switch ran it on the old default and loses it with the update. Set the switch before or with the update, in the server.envor in the Developers tab:bashEVOLVE_MODULE_EMAIL=trueThe
module_switcheshealth check has named these installs since 2.21 (emailunder "run on their default") and warns when a Brevo key is set while the module is off. Installs without campaigns or contact sync need nothing: the module was never used there.Form mails need nothing. The mail templates for form confirmations and notifications, the email blocks and their renderer moved into the core and work with the module off. Templates, their blocks and the form assignments stay as they are, the tables are unchanged. The panel entry moved from Email to the core section of the sidebar (
/evolve/mailtemplate, the old address redirects), Cmd-K finds "Mail templates" as before.Published mail views. Projects that published
email.viewskeep their copy ofrender/campaign.blade.php. Block copies undertheme/views/vendor/email/render/blocksare still read while the email module is on; move them totheme/views/vendor/evolve/email/blocks(publish tagevolve.mail-views) so they apply without the module too. A copy of the oldrender/document.blade.phpis no longer used, the layout isevolve::email.document.Published
config/email.php. The theme, merge tag dialect and asset base of the rendered mail live inevolve.mailofconfig/evolve.phpnow. A publishedconfig/email.phpthat still carriestheme,dialect,merge-tagsorasset-basekeeps winning over the core defaults, so nothing changes until the keys are removed. The environment variables (EMAIL_BRAND_COLOR,EMAIL_CONTAINER_WIDTH,EMAIL_MERGE_DIALECT,EMAIL_ASSET_BASE) keep their names.Code that references the module classes.
Racerfish\Email\Models\MailTemplate,Racerfish\Email\Templates\*andRacerfish\Email\Services\EmailDocumentRendererare deprecated subclasses of their core counterparts (Racerfish\Evolve\Models\MailTemplate,Racerfish\Evolve\MailTemplates\*,Racerfish\Evolve\Services\EmailDocumentRenderer) and go in 3.0. Both names read and write the same rows. Route names changed fromemail.template.*toevolve.mailtemplate.*, translation keys fromemail::templates.*toevolve::mail-templates.*.
To 2.21
Require laravel/mcp and laravel/ai where they are used. behaviour change Evolve no longer requires
laravel/mcpandlaravel/ai, it only suggests them. A project that uses the MCP server or the AI chatbot requires the package itself, in the same step as the update:bashcomposer require laravel/mcp # MCP server (stdio or HTTP) composer require laravel/ai # AI chatbot composer update racerfish/evolveWithout it,
composer updateremoves the package and the module stops; the site and the panel keep working. The MCP module is on by default, so check every project, including those that never connected an agent. Fleet shows which installs serve/mcp/evolve. After the update, the health endpoint reports a module without its package withactive: falseandmissing_packages, and the module switches check warns. A project that does not use MCP setsEVOLVE_MODULE_MCP=falseinstead.
To 2.19
Check the timezone. A new health check warns when
app.timezoneisUTC. Editors type Swiss local time into every date field, and Evolve stores and compares those values inapp.timezonewithout conversion, so on UTC a scheduled block or a publication date takes effect one to two hours late. To fix it, setAPP_TIMEZONE=Europe/Zurichin the server.env, make sureconfig/app.phpreads'timezone' => env('APP_TIMEZONE', 'UTC'), and runphp artisan config:cache. The editor input already stored then becomes correct, because it was Swiss time all along. Machine timestamps written before the switch (created_at,updated_at, the activity log) shift and show one to two hours early. A site that runs on UTC on purpose setsHEALTH_CHECK_TIMEZONE=false. See Health Checks.Migrate.
php artisan migrateadds thetracking.consent_cmpandtracking.cookiebot_idrows toevolve_settings.Footprints waits for consent on opt-in sites. behaviour change The
footprintsattribution cookie is only set after marketing consent. Opt-out sites see no change. Opt-in sites keep the attribution of a lead submitted in the same visit, but a visitor who never consents is no longer linked across visits. Declarefootprintsas a marketing cookie in the consent platform (see Cookies Evolve sets).Sites with Cookiebot. Choose Cookiebot as the consent platform on Settings > Tracking and enter the Cookiebot ID (or set
TRACKING_CMP=cookiebotandCOOKIEBOT_ID), then remove the project's own Cookiebot partial and its include, and the Cookiebot tag in the GTM container if there is one. Without this step an opt-in site with Cookiebot never writes the Evolve consent cookie, so server-side conversions, Hotjar and now alsofootprintsstay denied. The tracking health check warns about it. Sites with another platform callwindow.evolveConsentUpdate(), see Consent in the Theme.Copied theme views. The override check now also lists copies of
theme::views inresources/views/vendor/theme/:bashphp artisan evolve:view-overrides --since=2.18.0A copy of
theme::headadds@include('theme::consentPlatform')at its end. A copy oftheme::consentMode, or a project consent snippet rendered instead of it, takes over the second script block of the package view (the bridge and the Cookiebot listener). A project snippet that is not a copy is not detected, compare it by hand. A copy ofsettings.indexadds the platform select and the Cookiebot ID field to the consent card.Readable file links in the downloads block (optional). File URLs now carry the name (
/media/{hash}/{slug}.{ext}), and the copy link buttons use them. The downloads block lives in the project theme, so a project keeps linking/media/{hash}/download, which still works. To show the readable URL, let the link intheme/views/components/blocks/cms/downloads.blade.phpecho$media->filePath()as itshrefinstead of the hand built/media/…/downloadpath, and give it thedownloadattribute so a click still saves the file. See File URLs.Mail templates (email module).
php artisan migratealso addsconfirmation_template_idandnotification_template_idtoevolve_forms,localetoevolve_leadsand the tableemail_templates. Nothing changes until a template is assigned to a form: every form keeps its Markdown confirmation and notification. A copy ofform.indexadds the two template selects to the notifications card (evolve:view-overrides --since=2.18.0lists it), without them a form cannot pick a template. Projects that published the email render views (email.views) keep their copy ofrender/campaign.blade.php; the new layoutrender/document.blade.phpand the blockrender/blocks/leadSummary.blade.phpcome from the package. See Mail Templates.Google service account. behaviour change The package no longer ships a service account for the GA4 Data API (it was a Racerfish key, committed by mistake; the key is being rotated). A site that shows the analytics dashboard without its own
GOOGLE_APPLICATION_CREDENTIALSloses the page views after the update and sees a notice on the dashboard instead. Before updating, create a service account in the site's Google Cloud project with the Analytics Data API enabled, add its email as a viewer of the GA4 property, put the JSON file outside the web root and setGOOGLE_APPLICATION_CREDENTIALSto its path (absolute, or relative to the project root). Sites that already set the variable for Cloud Vision or auto-translate need nothing; the same file now serves the dashboard. A copy ofdashboard.indexadds the notice for the missing file (evolve:view-overrides --since=2.18.0lists it). See Environment Variables.
To 2.18
Update and migrate.
bashcomposer update racerfish/evolve php artisan migrateTwo migrations add rows to
evolve_settings: themailgroup andtracking.consent_via_cmp. Projects that want the laravel/mcp security fix also runcomposer update laravel/mcp -W(1.x is allowed now).Import the settings once per database. The panel now saves site name, branding, tracking, consent, verifications and mail addresses to the settings store. Until a site imports, nothing breaks: the
.envkeeps applying and the panel shows an import hint on General, Tracking and SEO. Take the values over on every environment with its own database:bashphp artisan evolve:settings:import --dry-run php artisan evolve:settings:importThe import fills only empty properties, skips package defaults and never edits the
.env. The.envlines stay as fallback for fields emptied later. From now on, change these values in the panel; editing them in the.envhas no effect once a value is stored.Set the notification address. behaviour change Forms without an administrator address used to mail the sender address. Now their leads go to the notification address, and without one the internal notification is skipped (a warning lands in the log; the visitor's confirmation still goes out). Check the form list in the panel for "No recipient" badges, then fill the notification address on Settings > Mail, or set
EVOLVE_MAIL_NOTIFICATION_ADDRESSbefore running the import. Health failure mails now go to the alert address on the same tab;HEALTH_WEBMASTER_ADDRESSkeeps working as its fallback.Consent on opt-in sites. A site running
opt-inwith the cookie banner off keeps rendering as before (denied by default), but the panel refuses to save the settings until the banner is on, the policy isopt-out, or the new Consent via CMP option is set. Sites with Cookiebot or another platform in GTM switch the CMP option on.Company identity. Enter legal name, address, contact, profiles and logo on Settings > Company to get the site-wide schema.org
Organization. Without a legal name nothing is emitted. Articles and blog posts lose theauthorandpublisherbuilt from acompany.nametranslation; projects that defined one move the data into the Company tab.Copied panel views. Run the check and bring every listed copy up to date:
bashphp artisan evolve:view-overrides --since=2.17.0Entries for 2.18.0 cover
settings.index(store inputs, Company and SEO tabs, the consent card, the validation errors at the top),form.index(notification address placeholders, tab title),activity.index(type labels and aliases),resource.create,resource.edit,user._formandinvite._form(browser tab title moved into the resource views),block.propType.address(Algolia Places removed) andcomponents.layout.sidebar(translated health label). A copy ofsettings.indexthat still posts the old variables has them dropped and logged, so its fields silently stop saving.MCP tokens. Over HTTP, write tools now check the panel permissions of the token's user. A token whose user lacks
pages.create,pages.edit,pages.publishorpages.deletegets a tool error instead of a change. Grant the role the permissions or issue the token for a user who has them. The local stdio transport is unaffected.New installations set these values in the panel and leave the store-backed variables (
APP_NAME,GTM_ID,MAIL_FROM_*and the rest of the bridge) out of the.env.
To 2.14
- Projects that published
blocks/cms/aiChatbot.blade.phpmust send the block ID asblockinstead ofsystem_prompt, otherwise the default instructions apply. Prompts composed on the client belong in an agent subclass viaai-chatbot.agent. - Projects with their own scroll up button in the bottom right corner should lift it while the widget is on.
To 2.13
Update the package locally and commit the lock file, then republish the panel assets.
bashcomposer update racerfish/evolve php artisan vendor:publish --tag=evolve.assets --forceCheck copied panel views. Projects that copy Evolve views into
resources/views/vendor/evolveortheme/views/vendor/evolvekeep rendering the old markup after the update. Run the check and bring every listed copy up to date, or delete copies that are no longer needed:bashphp artisan evolve:view-overrides --since=2.12.1 --strictIt also flags Font Awesome tags and white text on
bg-primaryin any copy, and copies ofpage.list, which the package no longer ships. Module views (blog::,event::and so on) are not covered, compare those by hand. See Overridden Panel Views.Custom panel code.
window.makeEditor,makeChart,makeTree,makeSelectand the date pickers return a Promise now, andwindow.makeCkEditoris gone. Icons in your own panel views use<x-evolve::icon name="…" />. Font Awesome strings in blueprints keep working through an alias table.Content Security Policy. The panel loads no web fonts any more, so
fonts.googleapis.comandfonts.gstatic.comare only needed if the website itself uses them.
To 2.12
Update the package locally and commit the lock file.
bashcomposer update racerfish/evolve php artisan migrateFour migrations run:
evolve_settings(the settings store), the permission catalogue (see Roles), and two for Shopify. Projects with a customisedevolve.roleskeep that config in place before migrating, because the permission grants are derived from it.Back up the live database before the first deploy. deletes data The deploy runs
migrate --forceon the server. On installs with the Shopify module, the foreign key migration first deletes rows inshopify_products_data,shopify_products_i18n,shopify_product_has_collectionsandshopify_collections_i18nwhose product or collection no longer exists, then adds cascading foreign keys. Those rows were unreachable, but they are gone afterwards. Take a dump on the server (see Shopify) and read the counts instorage/logsafter the deploy (SHOPIFY: Deleted <n> orphaned rows from ...).Copy changed theme files if the project publishes the starter theme:
theme/views/components/ui/tile.blade.php,theme/views/components/blocks/cms/{tiles,logoCloud,hero,simpleTextMultiCol,tabPanels}.blade.phpandtheme/src/lib/oembed-parser.ts, then rebuild the assets (#228). Ifvendor:publish --tag=shopify.assetsever ran, republishpublic/vendor/shopify/cart-store.js. A project with its own copy ofcomponents/image.blade.phpmirrors any changed widths or sizes inimage.conversions(Image Rendering).Sites embedded in an iframe on another domain: set
EVOLVE_FRAME_OPTIONS=(empty) in the server.env, and check thatpublic/.htaccessdoes not setX-Frame-Optionsitself (the starter's does). Otherwise the embed stops rendering after the deploy. See Security Headers.Decide on the queue for image conversions. With
QUEUE_CONNECTION=syncnothing changes: variants keep rendering on their first request. To move the work into a worker, setQUEUE_CONNECTION=database(thejobstable must exist), add the worker cron next toschedule:runand add the warm command to the after hooks:bash* * * * * cd ~/webhome/public_html/www/current && php84 artisan queue:work --stop-when-empty --max-time=55 >> /dev/null 2>&1php'hooks' => [ 'after' => [ 'php84 artisan evolve:images:warm --missing', ], ],Adjust the path to the environment's target and the binary to its
php-bin. The queue health check turns itself on as soon as the driver is notsync, so a stopped worker shows up on/evolve/health. Details in Image Rendering and Deployment.Deploy. The first deploy with 2.12 moves
storage/appof the release intoshared/storage/appnext toreleases/and links every release to it. No configuration, no manual step. Verify on the server:bashphp artisan evolve:ssh live ls -l current/storage/app # app -> ../../../shared/storage/app ls -d releases/*/storage/app.migrated # nothing: the move completedIf the deploy reports that releases could not be linked, the new release is live but no old release was purged; run the deploy again. Rolling back works as before and links the older release first. Backup scripts that read
releases/<release>/storage/appmust readshared/storage/appor go throughcurrent/. See Deployment.Page cache (sites with
APP_PAGE_CACHE=true). Entries written by earlier releases may hold a page rendered for a logged-in user, with the info bar and that session's CSRF token (#236).php artisan deployclears the page cache of the new release before it goes live, so a normal deploy removes them. If the site was updated any other way, or to be certain, clear and rebuild once on the server:bashphp artisan evolve:clear-page-cache php artisan evolve:rebuild-page-cacheSee Page Cache.
Move settings into the store (optional, per site). Review first, then import:
bashphp artisan evolve:settings:import --dry-run php artisan evolve:settings:importValues equal to the package default are skipped and keep following the package. The
.envis not changed. The panel still writes the.envin 2.12, and a stored value wins over it: after importing, edit those values in the store, not in the panel. See Settings Store.Check the security headers with
curl -sI https://<domain>/:X-Content-Type-Options,X-Frame-OptionsandReferrer-Policyon every environment,Strict-Transport-Securityonly on live over https. A Content-Security-Policy is opt-in; start withEVOLVE_CSP_REPORT_ONLY=true. Pages served from the static page cache bypass PHP and need the headers from.htaccess. See Security Headers.Sentry. With
sentry/sentry-laravelinstalled, events carry the tagssiteandenvironment_typeand the releaseevolve@<version>+<release directory>. Nothing to configure.php artisan evolve:env:verify livenow has aSentry DSNline that warns when the live.envhas no DSN. See Error Tracking.Shopify installs: the Admin API token of every store needs the
read_productsscope for the status sync. The daily purge of deleted products is opt-in (SHOPIFY_PURGE_DELETED_AFTER_DAYS). See Shopify.
Overridden Panel Views
A project can copy any Evolve view into resources/views/vendor/evolve/ (or theme/views/vendor/evolve/). Laravel then renders the copy instead of the package view. The copy never updates itself. When a release changes the package view, the project keeps rendering its old copy, and that usually fails quietly: a dead editor, a missing button, a script error in the browser console.
Since 2.13 the package keeps a log of view changes that a copy has to follow by hand, and a command reads it. Where copies go, which files can be published and which cannot is explained in Publishing and Overriding Package Files.
Check before every upgrade
Run this with the version the project is on now:
php artisan evolve:view-overrides --since=2.12.1The table lists every copied view and how many known changes it has to follow. Below the table each affected view gets its changes spelled out. A copy with none known is still a copy: it misses every future change as well, so it is worth asking whether it is still needed.
Add --strict to make the command exit with 1 while a copy has changes to follow, for example in a deploy script or CI job:
php artisan evolve:view-overrides --since=2.12.1 --strictBring a copy up to date
Compare the copy with the new package view:
shdiff resources/views/vendor/evolve/components/form/input.blade.php \ vendor/racerfish/evolve/core/views/components/form/input.blade.phpIf the project change is small, prefer deleting the copy and solving the need another way (a blueprint option, a config value, CSS in the theme). Every copy is a future upgrade task.
If the copy has to stay, start again from the new package view and reapply the project change on top. Patching the old copy line by line tends to miss changes the log does not describe.
Run the command again. The view drops out once you pass the new version as
--since.
Changes logged so far
| Version | View | What a copy has to change |
|---|---|---|
| 2.13.0 | components.form.input | window.makeEditor() returns a Promise. Replace editor = window.makeEditor(...) with window.makeEditor(...).then((instance) => editor = instance). |
| 2.13.0 | app, guest, blank | Remove the three Google Fonts <link> tags. The panel uses the system font stack. |
| 2.13.0 | any panel view with <i class="fal fa-…"> | Font Awesome is gone from the panel. Replace each tag with <x-evolve::icon name="…" /> (see Icons). The command finds these by content, whatever the view is called. |
| 2.13.0 | app, guest, blank | Replace the theme-color block on <body> with style="{{ \Racerfish\Evolve\Support\PanelTheme::cssVariables() }}" and read the favicon colour with PanelTheme::accent(). |
| 2.13.0 | components.layout.sidebar | Grouped into Core and Modules with .sidebar__label headings. Search and website sit as icon buttons next to the logo, media moved into Core, the system screens live in the user menu at the bottom. Start again from the package view. |
| 2.13.0 | components.layout.branding | Renders {{ $slot }} after the name, the sidebar puts search and website there. |
| 2.13.0 | components.layout.header | The mobile header shows only the menu button, logo and search. Everything else is in the sidebar, the header stack is gone. |
| 2.13.0 | components.toggle | The checked state is bg-primary (monochrome) instead of the green gradient. |
| 2.13.0 | any panel view with bg-primary and text-white | primary is the ink of the mode now. Use text-on-primary. Found by content. |
| 2.13.0 | resource.edit, resource.create | Title, breadcrumb and save buttons moved into <x-evolve::action-bar form="createForm" …> above the form. The back link and the button row below the form are gone, the form carries id="createForm". Start again from the package view. |
| 2.13.0 | page.show | The header (back link, page switcher, badges, live editing) moved into <x-evolve::action-bar form="pageForm" …>. The settings form carries id="pageForm", its three save buttons are gone. |
| 2.13.0 | settings.index, user.profile | Heading and save button moved into the action bar (form="settingsForm", form="profile"). The password form keeps its own button, labelled "Change password". |
| 2.13.0 | form.show, section.show | Back link and heading moved into the action bar. |
| 2.13.0 | user._form, invite._form | The hidden <x-evolve::form.redirect> field is gone. "Save" stays on the form, "Save & close" returns to the list. |
| 2.13.0 | activity.index, form.index, language.index, lead._form, navigation.index, page.index, redirect.index, region.index, section.index, staticstring.index, tag.index | Edit and create dialogs are side drawers. Replace <x-evolve::layout.modal> with <x-evolve::layout.drawer form="…">, give the form that id and drop its save button. A heading inside the dialog becomes the title prop. |
| 2.13.0 | user.index, invite.index | Users are edited in a drawer on the list, the create button became "Invite". Invitations are created in a dialog on the invite list. Start again from the package view. |
| 2.13.0 | components.form.input, block.editor | The editor toolbar and the bulk action head stick with the sticky-top class instead of sticky -top-8, so they stay below the action bar. |
| 2.13.0 | components.search | Rebuilt as the body of the Cmd-K palette (views, actions, content records, keyboard navigation). Start again from the package view. |
| 2.13.0 | components.layout.header | The search overlay moved into <x-evolve::layout.command-palette />, rendered after </header>. Replace the isSearchOpen block with that tag and drop the @role wrapper around the search button. |
| 2.13.0 | components.layout.sidebar | The search button next to the logo shows for every role. Drop its @role wrapper. |
| 2.13.0 | app | Optional. The Cmd/Ctrl+K listener calls event.preventDefault() and toggles the palette, openSearch() selects the input. Copies without it keep working. |
| 2.13.0 | components.dropdown | Optional. The menu logic is the registered Alpine component dropdown: use x-data="dropdown(@js($alignment))" and x-on:click="toggle()" on the trigger. Copies with the inline script keep working but repeat about 3.5 KB in every menu. |
| 2.13.0 | page.index | Optional. Visibility is a small icon in the row, changing it and the live link moved into the row menu. Copies with the old privacy dropdown keep working. |
| 2.13.0 | page.edit | The live editor header moved into <x-evolve::action-bar> inside a page-live-editor column, and the create page button is gone (Cmd+K has it). The Responsive, Desktop and Mobile switch is an <x-evolve::segmented> control bound with x-bind:aria-pressed. |
| 2.13.0 | block.editor | "Create block" is a secondary button everywhere (type="secondary" on <x-evolve::block.create-picker>). The solid button belongs to Save in the action bar. |
| 2.13.0 | media.index | The type filter is an <x-evolve::segmented> control with an "All" item, so clicking the active type no longer clears it. "Not attached" is a btn-toggle with aria-pressed, grid and list a second segmented control. |
| 2.13.0 | media._form | The full screen media viewer is a large drawer: <x-evolve::layout.drawer form="mediaForm" size="lg">, preview on top, fields below, delete and replace in the footer. Start again from the package view. |
| 2.13.0 | media._row | Optional. Slim row with a 32 px thumbnail. Copies with the old markup keep working, they are only taller. |
| 2.13.0 | health.index | Labels are translated (evolve::panel.health.*), results are cards sorted with failures first, the header buttons are secondary. |
| 2.13.0 | activity.index | The subject filter is an <x-evolve::segmented> control with an "All" item. |
| 2.13.0 | page.list | Removed. The view was not rendered anywhere, delete the copy. |
| 2.19.0 | theme::head | Ends with @include('theme::consentPlatform'), the consent platform script. |
| 2.19.0 | theme::consentMode | Second script block: window.evolveConsentUpdate() and the Cookiebot listener. Both script tags carry data-cookieconsent="ignore". |
| 2.19.0 | settings.index | The consent card gains tracking.consent_cmp and tracking.cookiebot_id. |
Since 2.19 the command also checks copies of the theme views (resources/views/vendor/theme/), listed with a theme:: prefix. The log itself lives in core/src/Support/ViewOverrides.php: CHANGES for changes to a specific view, PATTERNS for markup that breaks in any panel view copy. Frontend and mail views are not checked against PATTERNS, because they render with the project's own assets.
Panel Refresh (2.13 and Later)
The control panel is being redesigned over several releases (issue #210): monochrome in light and dark mode, less decoration, and a much smaller payload. This section grows with each release. Run the override check on every step.
2.13: Payload and fonts
Smaller bundle. The panel loads about 470 KB instead of 2.3 MB of JavaScript, CSS and fonts per page (158 KB instead of 638 KB with gzip). Charts, the rich text editor, the navigation tree, select boxes and date pickers load when a screen needs them.
Panel JavaScript API.
window.makeChart,makeEditor,editorAi,makeTree,makeSelect,makeDatePickerandmakeDateTimePickerkeep their names and arguments but now return a Promise. Code that ignores the return value keeps working. Code that uses it has to wait for it:js// before editor = window.makeEditor(el, content, bubbleMenu, $dispatch, options) // after window.makeEditor(el, content, bubbleMenu, $dispatch, options).then((instance) => editor = instance)CKEditor removed.
window.makeCkEditorno longer exists. Nothing in Evolve used it; the rich text editor is TipTap.Fonts. The panel uses the system font stack (San Francisco on macOS, Segoe UI on Windows, Roboto on Android) and loads no web font, so there is no flash of unstyled text and no request to Google Fonts. A Content Security Policy no longer needs
fonts.googleapis.comorfonts.gstatic.comfor the panel. Monospace text uses the system monospace font. This is a deliberate exception to the Racerfish Studios rule of Inter everywhere: the panel is a tool, and a font that loads on every page view costs more than it gives.After the update, republish the panel assets as always:
php artisan vendor:publish --tag=evolve.assets --force.
2.13: Icons
The panel no longer uses Font Awesome. Icons come from Nucleo and render as <svg><use> references into one sprite (/evolve/icons.svg), which the browser caches. No icon JavaScript runs any more, and icons no longer flicker after a Livewire update.
In Blade, use the component:
blade{{-- before --}} <i class="fal fa-fw fa-search"></i> {{-- after --}} <x-evolve::icon name="magnifier" /> <x-evolve::icon :name="$blueprint->icon" class="mr-2" />The icon is
1.125emsquare and follows the text colour, like a Font Awesomefa-fwicon did.Blueprints, module menus and the activity log keep their Font Awesome strings working.
icon: fad fa-film,->icon('fad fa-th-large')and stored names liketrash-altresolve through an alias table (core/assets/icons/aliases.php) to the closest Nucleo icon. Unknown names show a neutral cube. New blueprints should use the Nucleo name directly (icon: film). The names the panel ships are listed incore/assets/icons/manifest.php.Frontend views are not affected. A theme that uses Font Awesome on the website keeps loading it through its own assets. The Nucleo icons are licensed for the control panel only and must not be used in project frontends.
The private Font Awesome registry is gone.
core/assets/.npmrcwas removed, so building the panel assets needs no token any more.
2.13: Monochrome, light and dark
The panel keeps its light and dark mode (switch in the user profile, default follows the system), but the interface itself has no colour any more.
- Monochrome. The primary button, focus rings, the active navigation entry, checked toggles and checkboxes use the ink of the current mode: near black in light mode, near white in dark mode. In Tailwind,
primaryis that ink now andon-primarythe colour that reads on it. Status colours stay for status: green, red, amber and blue on badges, alerts and dots. - Brand colour.
evolve.theme-color(orAPP_BACKEND_THEME_COLOR, or the panel colour in the branding settings) still exists, default#E78401. It colours only the logo, the loading bar at the top, the upload bar and loading placeholders, available in Tailwind asaccent. The old default#1D4ED8, which published configs still carry as their fallback, counts as "not chosen" and gets the new default. - Neutrals. Dark mode: page
#262626, cards#2D2D2E, fields#363738, lines#424345. Light mode: page#F4F4F4, cards white, lines#E6E7E8. The Tailwindgrayscale of the panel follows these values, solight dark:class pairs keep working. - Navigation. The sidebar holds content only: Core (dashboard, pages, navigation, sections, forms, leads, media) and Modules. Search (⌘K) and the website link are icon buttons next to the logo. Users, languages, regions, static strings, redirects, tags, caches, activity, health and settings moved into the user menu at the bottom of the sidebar, which opens upwards. Roles decide what each person sees, as before. The region switcher stays above the user menu. On phones the header shows only the menu button, logo and search.
- Shapes. Two radii remain: 6 px for controls and 10 px for cards, lists and dialogs. The panel's own shadow set is gone.
- Copied views that put white text on
bg-primaryshow up in the override check: in dark mode that text is white on near white. Usetext-on-primary.
2.13: One action bar per screen
Every edit screen has one sticky bar at the top of the content: breadcrumb and title on the left, context actions and saving on the right. It stays in view on long forms, so saving no longer needs a scroll to the end.
One place to save. The save buttons in card footers and below forms are gone. The bar submits the form from outside through the
formattribute. While that form has unsaved input the bar says so, and leaving the page asks first.Only redirects on this site. Save & close submits
_redirect. Resource controllers now follow it only to a path or a URL on the panel's own host and otherwise stay on the form (or go to the list after creating). Before 2.13 any URL was followed.Save stays, Save & close leaves. "Save" reloads the form, "Save & close" returns to the list. The module forms (blog, event, FAQ, project, company, email) no longer carry a hidden
_redirectfield, which used to send every save back to the previous URL. Remove<x-evolve::form.redirect>from copied module forms as well.Breadcrumb instead of back links. The "Back" links above titles are gone, the breadcrumb in the bar leads back to the list.
In your own panel views use the component:
blade<x-evolve::action-bar :title="$item->title" :crumbs="[route('app.thing.index') => __('Things')]" form="thingForm" :close-url="route('app.thing.index')"> <x-slot:actions> <a href="{{ $item->url }}" class="btn btn-link">Show live</a> </x-slot:actions> </x-evolve::action-bar> <x-evolve::form id="thingForm" method="PUT" :action="route('app.thing.update', $item)"> … </x-evolve::form>Without
formthe bar shows only breadcrumb, title and actions.close-urladds "Save & close", which submits_redirectlike the old button did.widthtakes the max width class of the content column (width="max-w-5xl") so the bar lines up with a narrow screen. Abadgesslot sits right after the title.Resource controllers feed the breadcrumb of the generic edit and create screens with
$_resourceLabel. It is guessed from the{package}::views.{plural}translation (for exampleblog::views.posts) and falls back to the plural model name. Setprotected ?string $resourceLabel = 'app.things';(a translation key or plain text) on a controller where the guess is wrong.Sticky elements inside a screen use two classes that know about the bar:
sticky-topsticks right below it (editor toolbars, list heads),sticky-columnsticks a side column fromlgon with a gap below it. A copied module form that still usessticky top-0for its side column slides under the bar. Module views (blog::,event::and so on) are not covered by the override check, compare them by hand.Lists that edit in a dialog (media, redirects, tags, languages, static strings and similar) use a side drawer with its own Cancel and Save footer, see Records open in a side drawer.
2.13: Records open in a side drawer
Lists that edit a record without leaving the page (redirects, tags, languages, static strings, regions, sections, navigations, forms, leads, the activity log, the new page dialog and the module categories, teams and collections) open it in a drawer on the right instead of a centred dialog. The list stays visible behind it, ?edit={id} and ?create=true links keep working.
One place to save. The drawer has a fixed footer with Cancel and Save. Save submits the form inside through its
formattribute, so the save button inside the form is gone.In your own Livewire lists use the drawer like the modal, it takes the same props and closes the same way (
close-modal):blade<x-evolve::layout.drawer form="thingForm" :title="$this->isEditing ? __('evolve::panel.crud.edit') : __('evolve::panel.crud.create')"> <form id="thingForm" wire:submit="save"> … </form> <x-slot:footer> {{-- optional extra actions, left of Cancel and Save --}} </x-slot:footer> </x-evolve::layout.drawer>sizeisxs,sm(default),mdorlg. Withoutformthe drawer has no Save button, useful for read only details.save-labelrenames Save.php artisan evolve:resourcegenerates the drawer for new resources.Users and invitations. A user opens in a drawer on the user list (name, email, role, picture). Users are no longer created there: the old create page failed on the required password, users come from invitations. "Invite" opens a dialog with email and role, invitations are only created and deleted. Only developers can hand out the developer role, in the drawer, the dialog and the old
evolve/inviteform alike. Theevolve/user/{id}/editpage still works for existing links.Dialogs that are a task, not a record stay centred: the block editor, the content generator, the form builder fields and the language import preview.
Copied module views (
blog::category._formand the like) are not covered by the override check. A copy that still useslayout.modalkeeps working as a centred dialog.
2.13: Cmd-K jumps to screens and actions
Cmd+K (Ctrl+K on Windows and Linux) or the magnifier next to the logo opens a palette instead of the full screen search. It finds three things:
- Views. Every panel screen the user may open: pages, redirects, settings, each global collection, module lists and the developer tools.
- Actions. "New page", "New blog post", "New redirect", "Invite user" and similar. They open the create page, or the list with
?create=true, which opens its drawer. - Content. Pages, posts and the other searchable models, as before, below views and actions.
Arrow keys move through all three, Enter opens, Cmd/Ctrl+Enter opens in a new tab, Esc closes. Views and actions are matched in the browser, so they appear while typing. They match the label in the panel language, the English label and extra keywords, so "redirects" finds "Umleitungen".
- Access follows the routes. A command is only listed when its route exists and the user passes every
role:middleware on it. Module commands also respectevolve.roles.modules. Nobody sees a screen that would answer with 403. - Everyone gets the palette. The search button used to be limited to developers, admins and content managers. Now every panel user gets it, a CRM manager finds leads and the profile. Searching content stays limited to the roles in
evolve.roles.search(defaultdeveloper,admin,content_manager). Add that key to a publishedconfig/evolve.phponly if you want other roles. - Your own screens. Register them in a service provider or pass them with the module, see Global Search and Command Palette.
- Above drawers. The palette is a modal dialog, so Cmd+K also works while a drawer or dialog is open. Esc closes the palette and leaves the drawer open.
- Result headings. Content results are grouped under the model's panel name ("Seiten", "Beiträge"). Your own searchable models can name their group with a static
getSearchableGroupLabel(), otherwise the plural class name is used.
2.13: Slim list rows
Lists are denser and quieter: rows are 48 px high instead of about 64, separated by a fine line instead of alternating stripes, and the icon buttons in a row (the ⋮ menu, quick links) have no frame until hovered. The title carries the row.
- Page list. Visibility is a small monochrome icon before the title (eye, crossed eye, lock), with the state as tooltip. Changing it moved into the row menu under "Visibility", next to the bulk action that already did the same. The separate "show live" button is gone, the path is the live link and the menu has "Show page live". The homepage and the region show as a house icon and the region code instead of coloured badges.
- Smaller HTML. Every ⋮ menu used to carry its own 3.5 KB copy of the menu script, and created its positioning helper on page load. The script is now registered once, the helper is created on first open. The page list of the demo site went from 305 KB to 120 KB of Livewire HTML, every list with row menus shrinks the same way, and each search or sort sends less over the wire.
- Menus. A menu grows to 32 rem or 80 % of the window, whichever is smaller, before it scrolls. A
max-h-*class passed asmenu-classnow takes effect. The bulk selection menu opens to the right of its button. - Your own lists get the new rows through the
.list__itemclass and<x-evolve::resource.list-item>, nothing to change. A button in a row that should keep its frame uses something other thanbtn-secondary btn-square, for examplebtn-sm btn-secondary.
2.13: One solid button per view, media in a drawer
Every view now has at most one solid button, and it is the action that saves or creates. States such as an active filter or the chosen view mode are no longer solid buttons, so they cannot be mistaken for an action.
- Segmented control. A choice of exactly one option (view mode, type filter) is
<x-evolve::segmented>with<x-evolve::segmented.item>inside: a recessed track, the chosen item raised. Where "none" is a valid choice it is an explicit "All" item instead of clicking the active option a second time. Passactivefor state held by Livewire, or bindx-bind:aria-pressedfor state held by Alpine.icon-onlyshows the icon with the label as tooltip,label-class="hidden xl:inline"falls back to the icon on narrow screens. - On/off filters next to it use
btn btn-secondary btn-togglewitharia-pressed. When on, the button is tinted, never solid. - Page editor. Save in the action bar is the only solid button, "Create block" is outlined. The live editor has the same action bar at the top of its block column, and the preview size switch is a segmented control.
- Media. Opening a file shows it in a large drawer on the right instead of a full screen overlay: preview on top (a click on an image still sets the focal point), fields below, Delete and Replace file next to Cancel and Save. The list keeps its place behind it. The toolbar has "All, Images, Documents" as a segmented control, "Not attached" as a toggle and grid or list as a second segmented control. List rows follow the slim row style.
- Health. Translated, failing checks first, and the run and purge buttons are no longer solid.
- Activity log. The subject filter is a segmented control.
Pitfalls Found in Real Upgrades
These are the failures that actually cost time on client sites. Every one of them passes composer update and only shows up later, some of them only in the browser or only on the server. Walk this list before declaring an upgrade done.
Delete the Livewire 2 published assets
Symptom: the backend renders, but nothing reacts. Pagination does nothing, search does nothing, no wire:click anywhere in the panel fires. No PHP error, because the failure is a 404 on a script.
Cause: public/vendor/livewire/ is left over from the old vendor:publish --tag=livewire:assets step. Its manifest is the Livewire 2 format, where the value is a full query string rather than a bare hash:
{"/livewire.js":"/livewire.js?id=90730a3b0e7144480175"}Livewire 3 appends its own ?id= to that value and emits a URL that cannot resolve:
/vendor/livewire/livewire.js?id=/livewire.js?id=90730a3b0e7144480175Worse, the mere presence of that manifest makes Livewire 3 serve the published file instead of its own route, so even a corrected URL would load the Livewire 2 bundle.
Since v2.11 Evolve detects a stale bundle and republishes it on the next console boot (composer install, or the artisan optimize a deploy runs), evolve:deploy refuses to sync one, and evolve:env:verify reports it. On older versions, or to be sure:
Fix: delete the directory. Livewire 3 then serves /livewire/livewire.js from its own route.
rm -rf public/vendor/livewireWARNING
public/vendor is usually gitignored, so this deletion is not carried by a commit. Each machine needs it, and the servers only lose the stale copy through the rsync --delete of the next deploy. Verify after deploying:
php artisan evolve:ssh <environment>
ls public/vendor # expect `evolve` onlyRemoving the publish line from composer.json (Step 4) stops the directory being recreated. It does not remove what is already on disk.
Never run the upgrade with composer update --no-scripts
Symptom: the backend panel renders and nothing responds. The console shows, once per component:
Uncaught TypeError: Cannot read properties of undefined (reading 'entangle')
at Object.initializeComponent (app-XXXXXXXX.js)
at Object.discoverUninitializedComponents (app-XXXXXXXX.js)Cause: public/vendor/evolve is published by the evolve.assets hook in post-autoload-dump. --no-scripts skips it, so the previous major's backend bundle stays on disk. That bundle carries Alpine 2, while the v2 Blade views compile Livewire 3's @entangle into window.Livewire.find(...).entangle(...). Alpine 2 evaluates x-data on DOMContentLoaded, before window.Livewire exists, so find() returns undefined and the property access throws. Component initialization aborts and every wire:click in the panel is dead, pagination included.
This is the same Alpine 2 versus Livewire 3 skew described for 2.0.x, reached by a different route: there the package shipped Alpine 2, here a current package is running last year's published bundle.
Fix:
php artisan vendor:publish --force --tag=evolve.assetsThen delete the bundles the new manifest no longer references, so a later debugging session is not misled by files that are still being deployed.
Guard it. The published manifest must match the installed package's:
$this->assertSame(
json_decode(file_get_contents(base_path('vendor/racerfish/evolve/core/assets/public/build/manifest.json')), true),
json_decode(file_get_contents(public_path('vendor/evolve/manifest.json')), true),
);TIP
--no-scripts is a reasonable habit while resolving dependencies, since the hooks boot the app and the app cannot boot mid-upgrade. Just run composer dump-autoload (or the publish above) once the upgrade is complete.
A deploy can leave PHP-FPM serving the previous release
Symptom: after a successful deploy the site still references assets from the previous release. Requesting those files gives 404, because the web server serves static files from the new release while PHP still renders the old paths. php artisan tinker on the server renders the new paths correctly, which is what makes this confusing: the application is fine, only the FPM workers are stale.
Cause: opcache in the FPM pool, holding state resolved through the current symlink before it was repointed.
Fix: reset opcache from inside the pool. The CLI has its own opcache, so php -r 'opcache_reset();' over SSH does nothing for the web requests. Reset it through an HTTP request, then remove the file:
NAME="_opcache-reset-$(openssl rand -hex 8).php"
echo '<?php opcache_reset();' > "public/$NAME" # in the new release
curl -k "https://<host>/$NAME"
rm "public/$NAME"The random name keeps anyone else from calling the file in the seconds it exists.
Verify with the asset hash in the served HTML rather than by eye, and always after a deploy that changed published assets.
config/livewire.php overrides whole arrays, not single keys
Livewire merges the app config over its own with a shallow array_merge. A partial temporary_file_upload block therefore deletes every key it does not repeat, leaving uploads without disk, directory, middleware, preview_mimes, max_upload_time and cleanup.
Repeat the defaults and deviate only where you mean to:
'temporary_file_upload' => [
'disk' => null,
'rules' => 'file|max:50000', // the media manager needs more than the 12 MB default
'directory' => null,
'middleware' => null,
'preview_mimes' => [
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5,
'cleanup' => true,
],Check what survived:
php artisan tinker --execute='dump(array_keys(config("livewire.temporary_file_upload")));'Intervention Image 3 wants a driver class
Symptom: Unable to resolve driver. Driver object must implement Intervention\Image\Interfaces\DriverInterface.
The monorepo pulls intervention/image 3, which resolves the driver from a class. The version 2 string in config/image.php no longer works. Keep the env variable and map it:
'driver' => match (env('IMAGE_DRIVER', 'imagick')) {
'gd' => \Intervention\Image\Drivers\Gd\Driver::class,
default => \Intervention\Image\Drivers\Imagick\Driver::class,
},artisan optimize now compiles views
From Laravel 11, optimize includes view:cache. The deploy runs optimize on the server, so anything that cannot compile now aborts a deployment that used to succeed. Two things surface here:
- A view path that does not exist. Many projects still list
resource_path('views')inconfig/view.phpwhile usingtheme/views. Remove the dead entry. - Vendor views the theme cannot satisfy.
view:cachecompiles every registered view, including package views the project overrides at runtime. A blog module view calling<x-ui.card.image>fails if the theme only has a monolithiccard.blade.php. Add the missing sub-components in the theme's own idiom.
Run it locally before deploying, it is the same command the deploy runs:
php artisan optimize:clear && php artisan optimizeTo see every failure at once instead of stopping at the first:
php artisan tinker --execute='
$factory = app("view"); $finder = $factory->getFinder(); $compiler = app("blade.compiler");
foreach (array_merge($finder->getPaths(), ...array_values($finder->getHints())) as $path) {
if (! is_dir($path)) continue;
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)) as $file) {
if (! str_ends_with($file->getFilename(), ".blade.php")) continue;
try { $compiler->compile($file->getRealPath()); }
catch (Throwable $e) { echo $file->getRealPath(), "\n ", $e->getMessage(), "\n"; }
}
}'Resolving view rather than view.finder matters: the namespace hints are registered by a callback that only fires when view itself is resolved.
One Alpine, not two
A theme that bundles its own Alpine and calls Alpine.start() ends up with two instances once Livewire 3 ships its own. Point the alpinejs import at Livewire's instance and let Livewire start it.
// theme/src/lib/alpine.ts
import { Alpine } from 'livewire'
export default Alpine// theme/vite.config.mts
resolve: {
alias: {
'livewire': resolve(vendorDir, 'livewire/livewire/dist/livewire.esm.js'),
'alpinejs': resolve(__dirname, 'src/lib/alpine.ts'),
},
},app.ts then registers its stores as before and ends with Livewire.start() instead of Alpine.start().
Register no Alpine plugins
Livewire 3's bundle already carries persist, intersect, collapse and focus. Registering any of them again on the shared instance throws
Uncaught TypeError: Cannot redefine property: $persistand Alpine stops wiring directives. Nothing looks broken: the page renders, Alpine and Livewire are both on window, the stores exist. But every x-bind on the page is silently dropped, so anything whose classes come from a binding keeps only its static ones. A navbar that binds its background to $store.navigation simply loses it.
Delete these lines and their imports when moving a v1 theme over:
Alpine.plugin(PersistPlugin)
Alpine.plugin(IntersectPlugin)
Alpine.plugin(CollapsePlugin)Alpine's own directives keep working, so a smoke test will not catch this. The cheapest check is to run the built bundle against a real page in jsdom and assert on the element's class attribute after start.
When the theme bundles Livewire this way, the layout must emit only the runtime config, not the script tag, or a second Livewire loads:
{!! \Livewire\Mechanisms\FrontendAssets\FrontendAssets::styles() !!}
{!! \Livewire\Mechanisms\FrontendAssets\FrontendAssets::scriptConfig() !!}scriptConfig() also suppresses auto-injection, so this is the one case where it is correct. Use scripts() whenever the theme does not bundle Livewire itself.
Verify in the rendered HTML that livewire.js is loaded exactly once.
The theme build moved to public/build
The v2 theme package renders assets through Laravel's @vite, which reads public/build. Projects still copying dist/ into public/assets with rollup-plugin-copy will throw Vite manifest not found at: public/build/manifest.json on every page. Switch the theme to laravel-vite-plugin with publicDirectory: '../public' (it needs vite 5) and add /public/build to .gitignore.
envoy database:down does not drop the local database
The task imports the dump into the existing schema. The dump drops and recreates the tables it contains, but tables that exist only locally, such as those created by v2 migrations you already ran, survive. The dump also restores the source migrations table, so those migrations look pending again and re-running them fails with Base table or view already exists.
Drop the schema first:
mysql -u root -e 'drop database <db>; create database <db> character set utf8mb4 collate utf8mb4_unicode_ci;'
./vendor/bin/envoy run database:down --target=live
php artisan migrateFrom 2.22.3 on, evolve:db:copy empties the destination itself before it imports, so a copy into an environment that has already been deployed and migrated only needs the migrate afterwards. Before 2.22.3, empty the destination schema by hand before copying.
Verify against real data, not an old dump
Migrations that pass on a stale dump can still fail on production. Pull the current database down before running them, and treat a green run there as the actual check. The same stale-data trap explains most "the styling looks off locally" reports: the local database is simply showing a different page than the live site.
--from-file on evolve:env:setup is project relative
The path is resolved through base_path(), so an absolute path is reported as missing. Pass a path relative to the project root.
Troubleshooting
"Unable to locate file: vendor/racerfish/evolve/bin/Envoy.blade.php"
The Envoy import path changed in v2.0. Update your Envoy.blade.php to use vendor/racerfish/evolve/core/bin/Envoy.blade.php. See Step 12.
Class not found errors
Run composer dump-autoload to regenerate the autoloader.
Duplicate migration errors
Check for locally published migration files that conflict with package migrations. See Step 11.
Views not found
Clear the view cache: php artisan view:clear. If you have published views in theme/views/vendor/, verify the view namespace paths match (they haven't changed).
Livewire component not found
If you see "Unable to find component" errors, check that the namespace was updated from App\Http\Livewire to App\Livewire and that files were moved accordingly.
"Property not found" on wire:model
Ensure legacy_model_binding => true is set in config/livewire.php. See Step 6
"Call to undefined method LivewireManager::scripts()"
Your theme layout still uses @livewireScripts or @livewireStyles. See Step 7.
composer update stops at racerfish/evolve 2.0.2
Composer resolves ^2.0 against your current framework constraint. 2.1.0+ requires Laravel 12 (via laravel/ai ^0.4), so a project still on Laravel 11 is capped at 2.0.2. Run composer why-not racerfish/evolve 2.1.1 to confirm. Complete Step 3 to move to Laravel 12, then composer update racerfish/evolve will pick up 2.1.x.
Backend search or filter inputs throw "Cannot read properties of undefined (reading 'find')"
This is the v2.0.x Alpine 2 vs. Livewire 3 version skew. Evolve 2.0.x ships Alpine 2 in the backend bundle, but the backend Blade views use Livewire 3's @entangle, which compiles to window.Livewire.find(...).entangle(...). Alpine 2 evaluates x-data on DOMContentLoaded, before window.Livewire exists. The fix is to upgrade to Laravel 12 + Evolve 2.1.x, which moves the backend bundle to Alpine 3. See the warning at the top of this guide.