Skip to content
Retour au blog

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

Nuxt 4.6 apporte nuxt/server, la CLI v4, un $fetch typé réécrit et un avant-goût de Nuxt 5. Retour d'expérience sur la migration de ce site, option par option, avec ce qui marche, ce qui casse et les chiffres.
Désiré KOUASSI

Désiré KOUASSI

@kdesire09

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 champ engines du package.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 paquets nuxt@4.6.0, @nuxt/cli@4.0.0, etc. dans minimumReleaseAgeExclude. 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 :

server/api/contact.post.ts
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 :

server/api/contact.post.ts
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.

En Nuxt 4, les 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.

OptionRésultatGardée
routeTypedFetchTypes générés via fetchdts, aucune erreur sur ce projet✅
typedPagesA trouvé un typage trop lâche (voir ci-dessous)✅
prerenderErrorPages404.html passe d'une coquille SPA vide (9,8 Ko) à la vraie page 404 en HTML (27 Ko)✅
early404URL inconnue → 404 immédiate, i18n intact✅
inlineErrorRenderingerror.vue rendu dans la même requête, avec la page personnalisée✅
extractSerializablePageMeta, stripNeverHydratedDataRien de cassé, rien de mesurable sur ce site✅
future.compatibilityVersion: 5Build 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.2Nuxt 4.6.0
nuxt build (total)142,5 s131,8 s−7,5 %
Prérendu des 130 routes34,1 s26,3 s−23 %
Taille de .output/public61 Mo61 Mo=
Payload de la page d'accueil68,8 Ko68,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/server vaut le détour dès maintenant : c'est ce qui rendra le passage à Nuxt 5 et Nitro v3 transparent.
  • prerenderErrorPages et typedPages sont 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…).