mcpbeat

Nuxt Production

secondsky/nuxt-production

| Nuxt 5 production optimization: hydration, performance, testing with Vitest, deployment to Cloudflare/Vercel/Netlify, and migration from Nuxt 4. Use when: debugging hydration mismatches, optimizing performance and Core Web Vitals, writing tests with Vitest, deploying to Cloudflare Pages/Workers/Vercel/Netlify, or migrating from Nuxt 4 to Nuxt 5.

15k tokens
context cost
the whole folder, loaded on every use
7
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
202
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/secondsky/claude-skills --skill nuxt-production

The instruction itself

39 sections, as written by the author

Nuxt 5 Production Guide

Hydration, performance, testing, deployment, and migration patterns.

What's New in Nuxt 5

v5 Key Changes

| Change | Nuxt 4 | Nuxt 5 |

|--------|--------|--------|

| Bundler | Vite 6 (esbuild + Rollup) | Vite 8 (Rolldown) |

| Server engine | Nitro v2 | Nitro v3 (h3 v2) |

| Server errors | createError({statusCode}) | HTTPError({status}) |

| Client-only placeholder | Empty <div> | HTML comment node |

| callHook | Always returns Promise | May return void |

| clearNuxtState | Sets to undefined | Resets to initial default |

| Page names | Auto-generated | Normalized to route names |

| JSX support | Included by default | Optional (on-demand) |

| externalVue | Configurable | Removed (always mocked) |

Client-Only Comment Placeholders (v5)

Client-only components (.client.vue files and createClientOnly() wrappers) now render an HTML comment on the server instead of an empty <div>. This fixes scoped styles hydration issues.

<!-- If you relied on the placeholder <div> for layout -->
<ClientOnly>
  <MyComponent />
  <template #fallback>
    <div class="placeholder" style="min-height: 200px"></div>
  </template>
</ClientOnly>

To revert to the old <div> behavior:

// nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    clientNodePlaceholder: false
  }
})

Non-Async callHook (v5)

callHook may now return void instead of always returning Promise. Always use await:

// WRONG
nuxtApp.callHook('my:hook', data).then(() => { ... })

// CORRECT
await nuxtApp.callHook('my:hook', data)

When to Load References

Load references/hydration.md when:

  • Debugging "Hydration node mismatch" errors
  • Implementing ClientOnly components
  • Fixing non-deterministic rendering issues

Load references/performance.md when:

  • Optimizing Core Web Vitals scores
  • Implementing lazy loading and code splitting
  • Reducing bundle size

Load references/testing-vitest.md when:

  • Writing component tests with @nuxt/test-utils
  • Testing composables with Nuxt context
  • Mocking Nuxt APIs

Load references/deployment-cloudflare.md when:

  • Deploying to Cloudflare Pages or Workers
  • Configuring wrangler.toml
  • Setting up NuxtHub integration

Hydration Best Practices

What Causes Hydration Mismatches

| Cause | Example | Fix |

|-------|---------|-----|

| Non-deterministic values | Math.random() | Use useState |

| Browser APIs on server | window.innerWidth | Use onMounted |

| Date/time on server | new Date() | Use useState or ClientOnly |

| Third-party scripts | Analytics | Use ClientOnly |

Fix Patterns

<!-- Non-deterministic values -->
<script setup>
const id = useState('random-id', () => Math.random())
</script>

<!-- Browser APIs -->
<script setup>
const width = ref(0)
onMounted(() => {
  width.value = window.innerWidth
})
</script>

<!-- ClientOnly component -->
<template>
  <ClientOnly>
    <MyMapComponent />
    <template #fallback>
      <div class="skeleton">Loading map...</div>
    </template>
  </ClientOnly>
</template>

Performance Optimization

Lazy Loading Components

<script setup>
const HeavyChart = defineAsyncComponent(() =>
  import('~/components/HeavyChart.vue')
)
</script>

Lazy Hydration

<script setup>
const LazyComponent = defineLazyHydrationComponent(
  'visible',
  () => import('./HeavyComponent.vue')
)

const InteractiveComponent = defineLazyHydrationComponent(
  'interaction',
  () => import('./InteractiveComponent.vue')
)

const IdleComponent = defineLazyHydrationComponent(
  'idle',
  () => import('./IdleComponent.vue')
)
</script>

Route Caching

export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/about': { prerender: true },
    '/blog/**': { swr: 3600 },
    '/products/**': { isr: 3600 },
    '/dashboard/**': { ssr: false },
    '/static/**': {
      headers: { 'Cache-Control': 'public, max-age=31536000' }
    }
  }
})

Image Optimization

<template>
  <NuxtImg
    src="/images/hero.jpg"
    alt="Hero image"
    width="800"
    height="400"
    loading="lazy"
    placeholder
    format="webp"
  />

  <NuxtPicture
    src="/images/product.jpg"
    alt="Product"
    sizes="sm:100vw md:50vw lg:400px"
    :modifiers="{ quality: 80 }"
  />
</template>

Testing with Vitest

Setup

bun add -d @nuxt/test-utils vitest @vue/test-utils happy-dom
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    environmentOptions: {
      nuxt: {
        domEnvironment: 'happy-dom'
      }
    }
  }
})

Component Testing

import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import UserCard from '~/components/UserCard.vue'

describe('UserCard', () => {
  it('renders user name', async () => {
    const wrapper = await mountSuspended(UserCard, {
      props: {
        user: { id: 1, name: 'John Doe', email: '[email protected]' }
      }
    })

    expect(wrapper.text()).toContain('John Doe')
  })
})

Mocking Composables

import { mockNuxtImport } from '@nuxt/test-utils/runtime'

mockNuxtImport('useFetch', () => {
  return () => ({
    data: ref({ users: [{ id: 1, name: 'John' }] }),
    pending: ref(false),
    error: ref(null)
  })
})

Deployment

bun run build
bunx wrangler pages deploy .output/public
export default defineNuxtConfig({
  nitro: { preset: 'cloudflare_pages' }
})

Cloudflare Workers

export default defineNuxtConfig({
  nitro: { preset: 'cloudflare_module' }
})

Vercel / Netlify

// Vercel
export default defineNuxtConfig({
  nitro: { preset: 'vercel' }
})

// Netlify
export default defineNuxtConfig({
  nitro: { preset: 'netlify' }
})

NuxtHub (Cloudflare All-in-One)

bun add @nuxthub/core
export default defineNuxtConfig({
  modules: ['@nuxthub/core'],
  hub: {
    database: true,
    kv: true,
    blob: true,
    cache: true
  }
})

Migration from Nuxt 4

Step 1: Update package.json

{
  "devDependencies": {
    "nuxt": "^5.0.0"
  }
}

Step 2: Enable Compatibility Mode

// nuxt.config.ts
export default defineNuxtConfig({
  future: {
    compatibilityVersion: 5
  }
})

Step 3: Update Server Error Handling

// Before (Nuxt 4)
import { createError } from 'h3'
throw createError({ statusCode: 404, statusMessage: 'Not Found' })

// After (Nuxt 5)
import { HTTPError } from 'nitro/h3'
throw new HTTPError({ status: 404, statusText: 'Not Found' })

Step 4: Update Server Event API

// Before (Nuxt 4)
const path = event.path
event.node.res.statusCode = 200
setResponseHeader(event, 'x-custom', 'value')
const config = useRuntimeConfig(event)

// After (Nuxt 5)
const path = event.url.pathname
event.res.status = 200
event.res.headers.set('x-custom', 'value')
const config = useRuntimeConfig()

Step 5: Update Vite Config

// Before (Nuxt 4)
export default defineNuxtConfig({
  vite: {
    build: {
      rollupOptions: { ... }
    }
  }
})

// After (Nuxt 5) - use rolldownOptions
export default defineNuxtConfig({
  vite: {
    build: {
      rolldownOptions: { ... }
    }
  }
})

Step 6: Update Route Rules

// Before
routeRules: {
  '/old': { redirect: { to: '/new', statusCode: 302 } }
}

// After
routeRules: {
  '/old': { redirect: { to: '/new', status: 302 } }
}

Step 7: Update Import Paths

// Before
import { defineEventHandler, getQuery } from 'h3'

// After
import { defineEventHandler, getQuery } from 'nitro/h3'
// Or rely on auto-imports (no import needed)

Step 8: Remove Deprecated Options

// Remove these from nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    externalVue: false,  // REMOVED - delete this
    viteEnvironmentApi: true,  // REMOVED - always enabled
  }
})

Step 9: Install JSX Plugin (If Needed)

# Only if your project uses .jsx/.tsx files
bun add -D @vitejs/plugin-vue-jsx

Step 10: Update callHook Usage

// Before
nuxtApp.callHook('my:hook', data).then(() => { ... })

// After
await nuxtApp.callHook('my:hook', data)

Common Anti-Patterns

Client-Only Code on Server

// WRONG
const width = window.innerWidth

// CORRECT
if (import.meta.client) {
  const width = window.innerWidth
}

// Or use onMounted
onMounted(() => {
  const width = window.innerWidth
})

Non-Deterministic SSR

// WRONG
const id = Math.random()
const time = Date.now()

// CORRECT
const id = useState('id', () => Math.random())
const time = useState('time', () => Date.now())

Troubleshooting

Hydration Mismatch:

  • Check for window, document, localStorage usage
  • Wrap in ClientOnly or use onMounted
  • Look for Math.random(), Date.now()
  • Check if relying on <div> placeholder for client-only components

Build Errors:

rm -rf .nuxt .output node_modules/.vite && bun install

Vite Plugin Warnings:

  • Migrate from extendViteConfig({ server }) to configEnvironment
  • Use applyToEnvironment instead of server: false / client: false

Rolldown Build Issues:

  • Replace rollupOptions with rolldownOptions
  • Replace vite.esbuild with vite.oxc
  • Check CJS interop changes in Vite 8
  • nuxt-core: Project setup, routing, configuration
  • nuxt-data: Composables, data fetching, state
  • nuxt-server: Server routes, API patterns (Nitro v3)
  • cloudflare-d1: D1 database patterns

Version: 5.0.0 | Last Updated: 2026-03-30 | License: MIT

How to use it

Copy the folder

Take secondsky/nuxt-production from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.