Nuxt 4.6 : j'ai migré mon portfolio et activé (presque) tous les flags

Nuxt 4.6 est sorti le 5 octobre 2026. C'est l'une des plus grosses versions mineures du framework : plus de 420 commits depuis la 4.5.2, une nouvelle CLI majeure, et surtout une bonne partie de Nuxt 5 déjà disponible derrière des flags. Nuxt 3 étant en fin de vie depuis le 31 juillet, c'est aussi le moment de se mettre à jour.
Plutôt que de résumer les notes de version, j'ai migré ce portfolio (Nuxt Content, i18n, Nuxt UI, Nuxt SEO, Workflow, evlog) et testé chaque nouveauté une par une. Voici ce qui a marché, ce qui a cassé, et les chiffres.
La migration elle-même
La commande recommandée est :
npx nuxt upgrade --dedupe
Chez moi, elle a annoncé « already using the latest version (4.5.2) » sans rien mettre à jour, et a même déplacé @nuxt/kit dans les devDependencies au passage. J'ai donc fait la mise à jour à la main :
pnpm add -D nuxt@^4.6.0
pnpm add @nuxt/kit@^4.6.0
Deux détails à surveiller :
- Node.js : la 4.6 exige
^22.22.3 || ^24.15.0 || >=26.0.0. J'ai aligné le champenginesdupackage.json. - pnpm
minimumReleaseAge: si vous l'utilisez (et vous devriez), une version publiée la veille est bloquée. pnpm a ajouté lui-même les paquetsnuxt@4.6.0,@nuxt/cli@4.0.0, etc. dansminimumReleaseAgeExclude. Pensez à le commiter.
Sans activer aucune option, le site a fonctionné du premier coup : toutes les pages, les deux langues, le contenu.
Nuxt CLI v4 : le changement qu'on voit tout de suite
En lançant nuxt dev, on découvre la nouvelle CLI, livrée avec Nuxt. Le terminal devient une petite interface : progression du démarrage, raccourcis (o ouvrir, l logs, n réseau, p routes), et un résumé des requêtes en échec qu'on peut tracer.

L'écran de chargement du navigateur a lui aussi été redessiné : une chaîne de montagnes en particules WebGL2 (l'image en haut de cet article est une capture de cet écran). Il désactive l'animation avec prefers-reduced-motion et retombe sur le logo seul sans WebGL2.
Petit bonus pratique, la recherche dans la doc depuis le terminal :
nuxt docs "server imports"
Elle télécharge et indexe la documentation de votre version de Nuxt, puis ouvre la bonne page.
nuxt/server : du code serveur qui survivra à Nuxt 5
Jusqu'ici, le code serveur importait (ou auto-importait) les helpers de h3 et nitropack. Chaque version majeure de h3 ou de Nitro cassait donc tout l'écosystème. La 4.6 introduit nuxt/server, une surface d'import maintenue par Nuxt et basée sur les standards du web (Request, Headers, URL).
J'ai migré le handler du formulaire de contact. Avant :
import type { H3Event } from 'h3'
export default defineEventHandler(async (event: H3Event) => {
const result = await readValidatedBody(event, body => contactSchema.safeParse(body))
if (!result.success) {
throw createError({ statusCode: 400, statusMessage: result.error.errors[0].message })
}
// ...
})
Après :
import { defineEventHandler, readValidatedBody } from 'nuxt/server'
export default defineEventHandler(async (event) => {
const data = await readValidatedBody(event, contactSchema, {
onError: ({ issues }) => ({
status: 400,
statusText: issues?.[0]?.message ?? 'Validation failed',
issues,
}),
})
// ...
})
Zod v4 implémente Standard Schema, donc on passe directement le schéma, sans safeParse.
defineEventHandler, readBody… auto-importés restent ceux de h3. Pour utiliser le nouveau runtime, il faut importer explicitement depuis nuxt/server, y compris defineEventHandler. Mélanger les deux déclenche une erreur NUXT_E8012.Attention aussi aux différences de forme : createError prend status/statusText (et non plus statusCode/statusMessage), et sendRedirect renvoie la réponse au lieu de l'envoyer.
Les options expérimentales, une par une
J'ai activé chaque option, puis vérifié le build de production, les types (vue-tsc -b) et les pages en FR et en EN.
| Option | Résultat | Gardée |
|---|---|---|
routeTypedFetch | Types générés via fetchdts, aucune erreur sur ce projet | ✅ |
typedPages | A trouvé un typage trop lâche (voir ci-dessous) | ✅ |
prerenderErrorPages | 404.html passe d'une coquille SPA vide (9,8 Ko) à la vraie page 404 en HTML (27 Ko) | ✅ |
early404 | URL inconnue → 404 immédiate, i18n intact | ✅ |
inlineErrorRendering | error.vue rendu dans la même requête, avec la page personnalisée | ✅ |
extractSerializablePageMeta, stripNeverHydratedData | Rien de cassé, rien de mesurable sur ce site | ✅ |
future.compatibilityVersion: 5 | Build et pages OK, mais le routage devient sensible à la casse | ⏸️ |
vue.vapor (Vue 3.6 RC) | Build cassé par @nuxtjs/mdc | ❌ |
server.builder: 'vite' | Incompatible avec ce site | ❌ |
Le gain le plus concret est prerenderErrorPages. Sur Vercel, le 404.html statique servait jusqu'ici une page vide hydratée côté client. C'est désormais la vraie page « Oups ! », lisible sans JavaScript et par les robots.
typedPages + i18n
Avec typedPages, useRoute() sans argument a renvoyé unknown dans pages/blog/[...slug].vue. Avec @nuxtjs/i18n, les routes portent un suffixe de langue. Il faut donc donner les noms au générique :
const route = useRoute<'blog-slug___fr' | 'blog-slug___en'>()
const slug = computed(() => route.params.slug ?? [])
Bonus : TypeScript sait maintenant que slug est un string[], et l'ancien cast défensif Array.isArray(...) ? ... as string[] : [... as string] a pu disparaître.
compatibilityVersion: 5
Le build passe, toutes les pages et l'API répondent. Le changement visible est le routage sensible à la casse : /Contact ou /BLOG renvoient maintenant 404. C'est cohérent avec Nitro, mais je préfère activer ce flag au moment de passer à Nuxt 5 plutôt que de risquer de casser d'anciens liens.
Vapor : bloqué par un module
La 4.6 permet de passer un composant en Vapor Mode avec <script setup vapor> (avec Vue 3.6, encore en RC). J'ai essayé sur le footer du site, présent sur toutes les pages. Le build a échoué dans le compilateur Vapor :
TypeError: Cannot read properties of undefined (reading '0')
at viteMDCSlot (@nuxtjs/mdc/dist/module.mjs)
La transformation de template de @nuxtjs/mdc lit context.nodeTransforms, qui n'existe pas dans le contexte du compilateur Vapor. C'est le même code dans la dernière version (0.23.2). Tant que mdc n'est pas compatible, un seul composant Vapor suffit à casser le build. Je n'aurais de toute façon pas mis une RC de Vue en production.
server.builder: 'vite'
Le nouveau builder serveur expérimental fabrique l'app avec Vite seul, sans Nitro. Il prévient lui-même : pas de prérendu, pas de routes serveur, pas de plugins Nitro. Le build s'arrête sur un module qui appelle useNitro(). Ce site repose sur le prérendu, une route API, Workflow, evlog et les images OG : Nitro reste le bon choix, comme le dit d'ailleurs l'équipe Nuxt.
Les chiffres
Mesurés sur ma machine (Apple Silicon, Node 24.18), build de production complet après suppression de .nuxt, une seule mesure à chaque fois (à prendre comme une tendance) :
| Nuxt 4.5.2 | Nuxt 4.6.0 | ||
|---|---|---|---|
nuxt build (total) | 142,5 s | 131,8 s | −7,5 % |
| Prérendu des 130 routes | 34,1 s | 26,3 s | −23 % |
Taille de .output/public | 61 Mo | 61 Mo | = |
| Payload de la page d'accueil | 68,8 Ko | 68,8 Ko | = |
Sur un site déjà entièrement prérendu, la sortie ne change pas. Le gain vient du temps de build, surtout du prérendu.
Ce que je retiens
- La migration est sans douleur : aucune modification de code n'a été nécessaire pour que le site tourne en 4.6.
nuxt/servervaut le détour dès maintenant : c'est ce qui rendra le passage à Nuxt 5 et Nitro v3 transparent.prerenderErrorPagesettypedPagessont des gains concrets, à activer sans attendre.- Vapor et le builder Vite sont des expérimentations, à suivre de près, mais à ne pas mettre en production sur un vrai projet.
Toutes les nouveautés sont dans les notes de version officielles de Nuxt 4.6, y compris celles que je n'ai pas couvertes ici (addons pour useFetch, appSecret et sessions, payloads allégés…).