L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Upgrade a server-rendered Craft CMS/Twig front end: Laravel Mix or Webpack to Vite via craft-vite (manifest, DDEV dev server, HMR, Tailwind v3, legacy builds, Twig asset URLs) and Vue 2 to Vue 3 (@vue/compat, Vuex to Pinia, plugin swaps, Vue islands in Twig). Use when a site still has webpack.mix.js, mix-manifest.json or Vue 2, or when deciding if a small V…
Independent GitHub snapshot · Automated classification · Original documentation governs
name: frontend-upgrade-ops
description: "Upgrade a server-rendered Craft CMS/Twig front end: Laravel Mix or Webpack to Vite via craft-vite (manifest, DDEV dev server, HMR, Tailwind v3, legacy builds, Twig asset URLs) and Vue 2 to Vue 3 (@vue/compat, Vuex to Pinia, plugin swaps, Vue islands in Twig). Use when a site still has webpack.mix.js, mix-manifest.json or Vue 2, or when deciding if a small Vue widget should become Alpine.js or vanilla JS instead."
license: MIT
allowed-tools: "Read Edit Write Bash Glob Grep"
metadata:
author: claude-mods
related-skills: "migrate-ops, vue-ops, craftcms-ops, ddev-ops, web-perf-ops, tailwind-ops, playwright-ops, security-ops, package-manager-ops"
Frontend Upgrade Operations
The two front-end upgrades a Craft CMS (or any Twig/PHP) agency keeps meeting: an asset
pipeline still on Laravel Mix or raw Webpack, and widgets still on Vue 2. Both
are frozen - Mix's last release was 6.0.49 in June 2022 and Laravel calls it "a legacy
package that is no longer actively maintained"; Vue 2 reached end of life on
31 December 2023. This skill is the playbook for moving a server-rendered site to
Vite 8 + craft-vite and Vue 3, or for deciding that a widget should not be Vue
at all.
Version facts verified as of 2026-10-05 against npm, Packagist, vite.dev,
v3-migration.vuejs.org and nystudio107.com/docs/vite. Each reference cites its sources.
Out of scope here - steady-state Vue is vue-ops, steady-state Craft is craftcms-ops
-
Migrate Vue, or replace it?
Most agency Vue 2 code is not an app; it is a handful of widgets. Migrating a widget
that a dozen lines of Alpine would do costs more than replacing it. Decide per
island, before estimating the migration.
The widget is...
Verdict
Why
Toggle, accordion, tabs, mobile nav, show/hide
Native HTML or Alpine.js
<details>, <dialog>, popover cover much of it with zero JS; the rest is a boolean in x-data written straight into Twig
A form with validation and a submit spinner
Alpine + native constraint validation
No component tree, no store; Alpine keeps the markup in Twig where editors expect it
A thin wrapper around a framework-free library (slider, date picker, map, lightbox)
Vanilla JS or Alpine calling the library directly
The Vue wrapper was the only Vue 2-specific part
Renders server data and never changes it
Delete it; render in Twig
It's a template, not an app
Filtering/sorting a list with derived counts, several child components
Vue 3
Computed state and components earn their weight
Several islands sharing state (cart, saved items, auth)
Vue 3 + Pinia
A shared store across apps is what Vue + Pinia does well
Routing inside a section (multi-step tool, dashboard)
Vue 3 (+ vue-router)
Alpine has no answer for this
Built on a Vue 2 UI kit with no Vue 3 release
Rewrite budget, Vue 3 or Alpine, decided by the rows above
The kit, not Vue, is the migration
Rules of thumb for the grey zone: under ~150 lines with one level of state, Alpine
wins; any use of Vuex, a router, or more than two child components, Vue 3 wins. A site
that ends up with both is normal - see the Alpine coexistence rules in
vue-islands-in-twig.md.
Sequencing both upgrades
Each numbered step is a separate deploy with its own parity check. Combining the
bundler swap with the framework swap is the classic mistake: when a page breaks you
cannot tell which change did it.
0. Inventory rg the repo (below); list page types that render JS widgets
1. Vue 2.7.16 if on Vue 2.6 - backports Composition API + <script setup>
2. Mix -> Vite still on Vue 2: Vite 7.3 + @vitejs/plugin-vue2
(plugin-vue2's peer range ends at vite ^7.0.0 as of 2026-10-05;
Vite 7.3 still gets important fixes + security patches)
3. Replace widgets the table above says shouldn't be Vue -> Alpine/vanilla
4. Vuex -> Pinia 2 (optional, still on Vue 2.7; Pinia 3+ dropped Vue 2)
5. Vue 3 @vue/compat on Vite 8 + @vitejs/plugin-vue; burn down warnings
6. Drop compat then Pinia 4, then remove @vitejs/plugin-legacy if analytics allow
No Vue on the site? Steps 1, 3-6 vanish: go straight to Vite 8.
If Vue 2 must die first (a security finding, a hosting rule), the order flips: Vue 3
via @vue/compaton Webpack is supported by the migration guide, then Vite. It is
the slower path - two configurations of Vue 3 tooling instead of one.
# Step 0 - inventory in one pass
fd -H "webpack.mix.js|webpack.config.js|mix-manifest.json|vite.config" -E node_modules
jq '{vue:.dependencies.vue, mix:.devDependencies["laravel-mix"], webpack:.devDependencies.webpack, vite:.devDependencies.vite, vuex:.dependencies.vuex}' package.json
rg -n "mix\(|craft\.vite|data-vue-island|inline-template|new Vue\(" templates/ src/ | head -50
Cutover checklist: Mix/Webpack to Vite
Work top to bottom; each line links to the detail.
Branch from a green build; record a parity baseline - screenshots of every
page type + the network panel's JS/CSS list on the Mix build, plus lab LCP/CLS/TBT
for the key templates (median of 5 runs, method in web-perf-ops)
npm pkg set type="module" (or name configs .mjs/.cjs explicitly)
vite.config.js: base, build.manifest, outDir: web/dist, entries in
build.rolldownOptions.input (config)
DDEV: the same port in web_extra_exposed_ports, server.port, server.origin,
devServerPublic; cors + allowedHosts for .ddev.site
(DDEV)
Twig: every mix() / hard-coded /js/, /css/ tag replaced with
craft.vite.script(...); main CSS with asyncCss = false unless critical CSS exists
Inline scripts that used bundle globals at parse time fixed (module scripts are
deferred) (why)
Twig-referenced images resolved via craft.vite.asset() or left in web/
Legacy build: decided from analytics, @vitejs/plugin-legacy only if needed
(legacy)
HMR works for CSS + JS; Twig edits reload (vite-plugin-restart)
vite build in CI/deploy; web/dist/ gitignored; no hashed files committed
Parity check against the baseline: every page type, fonts, icons, console clean,
and the lab metrics - async CSS and modulepreload move LCP and CLS
(web-perf-ops has the LCP/CLS fixes)
Deploy keeps the previous build until full-page caches are purged
Delete webpack.mix.js, mix-manifest.json, old web/js + web/css,
laravel-mix and webpack-only devDependencies - in a follow-up commit
Vue 2 to 3 at a glance
The full workflow, the breaking-change table and testing live in
vue2-to-vue3.md. The shape:
Alias vue to @vue/compat, set compatConfig: { MODE: 2 } in @vitejs/plugin-vue.
Port new Vue({ el }) to createApp(...).mount(el) - per island.
Fix compile errors, rename transition classes, upgrade Vuex/router, then runtime
warnings one compat ID at a time (compatConfig: { MODE: 3 } per clean component).
Global MODE: 3, zero warnings, remove compat.
Three Twig-site landmines worth knowing before you start:
inline-template is removed - Twig-authored templates need a scoped slot, a
<script type="text/html"> template, or conversion to an SFC island.
Removing compat removes the template compiler.@vue/compat resolves to the full
build, plain vue to runtime-only; in-DOM islands go blank unless vue is aliased to
vue/dist/vue.esm-bundler.js.
In-DOM templates execute {{ }} from content - client-side template injection.
SFC islands with props avoid it (security).
Common gotchas
Gotcha
Prevention
Bundler and framework upgraded in one deploy
Sequencing above; one parity check per step
Tailwind v4 "while we're at it"
Separate migration with its own visual diff (tailwind-ops)
cors: true / allowedHosts: true copied from a sample
Regex + .ddev.site; true opens the dev server to DNS rebinding
Each island calls createPinia()
One exported instance, app.use(pinia) everywhere
@vitejs/plugin-vue2 on Vite 8
Its peer range ends at Vite 7; finish Vue 3 before Vite 8, or stay on Vite 7.3 meanwhile
Secrets moved from MIX_ to VITE_ vars
VITE_* is inlined into public JS - keep secrets server-side
Old Mix files deleted in the cutover deploy
Delete in a follow-up once nothing references them
Estimating Vue 3 before checking plugins
One Vue 2-only plugin can be the critical path - inventory first
# Structural (PR CI, no network): every catalogued package/fact is still named in the
# prose, and SKILL.md keeps a dated "as of" note.
python scripts/check-frontend-upgrade-facts.py --offline # exit 0 consistent, 10 drift
# Live (freshness job, never blocks a PR): npm + Packagist majors vs documented majors.
python scripts/check-frontend-upgrade-facts.py --live # exit 10 a major moved, 7 unreachable
A --live drift means the world moved (say, Vite 9 or Pinia 5): re-verify the affected
reference against its cited source, then bump documented_major - never just the number.
See also
Skill
When to combine
migrate-ops
Generic upgrade strategy, rollback, codemods, other frameworks