Add nuxt-skills and update auto quotes to use new policy API structure
- Add nuxt-skills (vue, nuxt, nuxt-ui) to .claude/skills/ - Create useCustomerSelection() composable for managing insured/buyer selection - Create usePolicyApi() composable for policy API operations - Update auto quote components to use insured/buyer instead of client - Update vehicle fields: remove valorVehiculo, add market_value, requested_value, rc_limits - Make chassis_number and engine_number optional - Update auto quote types and composables to match new API structure - Update auto quote page to submit to policy API with new structure
This commit is contained in:
278
.claude/skills/nuxt/references/middleware-plugins.md
Normal file
278
.claude/skills/nuxt/references/middleware-plugins.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# Nuxt Middleware & Plugins
|
||||
|
||||
## When to Use
|
||||
|
||||
Working with `middleware/` or `plugins/` directories, route guards, app extensions.
|
||||
|
||||
## Route Middleware
|
||||
|
||||
Route middleware runs before navigation. Used for auth checks, redirects, logging.
|
||||
|
||||
### Global Middleware
|
||||
|
||||
Runs on every route change. **REQUIRED: Use `.global.ts` suffix:**
|
||||
|
||||
```ts
|
||||
// middleware/auth.global.ts
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (to.meta.requiresAuth && !auth.isAuthenticated) {
|
||||
return navigateTo('/login')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Without `.global.ts` suffix, middleware is named (not global).**
|
||||
|
||||
## Red Flags - Stop and Check Skill
|
||||
|
||||
If you're thinking any of these, STOP and re-read this skill:
|
||||
|
||||
- "Suffix doesn't matter, it's about where I put it"
|
||||
- "I'll redirect() instead of return navigateTo()"
|
||||
- "I remember Nuxt 3 middleware patterns"
|
||||
- "Export default function is simpler"
|
||||
|
||||
All of these mean: You're using outdated patterns. Use Nuxt 4 patterns instead.
|
||||
|
||||
### Named Middleware
|
||||
|
||||
Runs only when explicitly applied. No `.global` suffix:
|
||||
|
||||
```ts
|
||||
// middleware/admin.ts
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (!auth.isAdmin) {
|
||||
return navigateTo('/')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Apply in page:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['admin']
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Middleware Return Values
|
||||
|
||||
```ts
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
// Allow navigation
|
||||
return
|
||||
|
||||
// Redirect
|
||||
return navigateTo('/login')
|
||||
|
||||
// Abort navigation
|
||||
return abortNavigation()
|
||||
|
||||
// Abort with error
|
||||
return abortNavigation('Not authorized')
|
||||
})
|
||||
```
|
||||
|
||||
### Middleware Order
|
||||
|
||||
1. Global middleware (alphabetical by filename)
|
||||
2. Layout middleware (if layout defines middleware)
|
||||
3. Page middleware (defined in definePageMeta)
|
||||
|
||||
## Plugins
|
||||
|
||||
Plugins extend Vue app with global functionality. Run during app initialization.
|
||||
|
||||
### Basic Plugin
|
||||
|
||||
```ts
|
||||
// plugins/my-plugin.ts
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
return {
|
||||
provide: {
|
||||
hello: (name: string) => `Hello ${name}!`
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Use in components:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { $hello } = useNuxtApp()
|
||||
console.log($hello('World')) // "Hello World!"
|
||||
</script>
|
||||
```
|
||||
|
||||
### Plugin with Vue Plugin
|
||||
|
||||
```ts
|
||||
import type { PluginOptions } from 'vue-toastification'
|
||||
// plugins/toast.client.ts
|
||||
import Toast from 'vue-toastification'
|
||||
import 'vue-toastification/dist/index.css'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.use(Toast, {
|
||||
position: 'top-right',
|
||||
timeout: 3000
|
||||
} as PluginOptions)
|
||||
})
|
||||
```
|
||||
|
||||
### Plugin with Hooks
|
||||
|
||||
```ts
|
||||
// plugins/init.ts
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook('app:created', () => {
|
||||
console.log('App created')
|
||||
})
|
||||
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
console.log('Page finished loading')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Client-Only or Server-Only
|
||||
|
||||
Use file suffix:
|
||||
|
||||
- `.client.ts` - runs only on client
|
||||
- `.server.ts` - runs only on server
|
||||
|
||||
```ts
|
||||
// plugins/analytics.client.ts
|
||||
export default defineNuxtPlugin(() => {
|
||||
// Only runs in browser
|
||||
if (window.analytics) {
|
||||
window.analytics.init()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Plugin Order
|
||||
|
||||
Use numeric prefix for execution order:
|
||||
|
||||
```
|
||||
plugins/
|
||||
├── 01.first.ts
|
||||
├── 02.second.ts
|
||||
└── 03.third.ts
|
||||
```
|
||||
|
||||
### Async Plugins
|
||||
|
||||
```ts
|
||||
// plugins/api.ts
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const config = await fetch('/api/config').then(r => r.json())
|
||||
|
||||
return {
|
||||
provide: {
|
||||
config
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Middleware:**
|
||||
|
||||
- **Return navigation or nothing** - don't mutate state heavily
|
||||
- **Keep logic minimal** - delegate to composables/stores
|
||||
- **Use for guards & redirects** only
|
||||
- **Check meta properly** - `to.meta.requiresAuth`
|
||||
- **Global = `.global.ts`** suffix required
|
||||
|
||||
**Plugins:**
|
||||
|
||||
- **Use for app-wide functionality** only
|
||||
- **Provide via `provide`** for type safety
|
||||
- **Consider client/server context** - use `.client`/`.server`
|
||||
- **Minimize work** in plugin initialization
|
||||
- **Use hooks** for lifecycle events
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| ❌ Wrong | ✅ Right |
|
||||
| ------------------------------------ | ------------------------------------------------------------ |
|
||||
| `export default function({ route })` | `export default defineNuxtRouteMiddleware((to, from) => {})` |
|
||||
| Mutate route object | Return navigateTo() or nothing |
|
||||
| `middleware/auth.ts` (not global) | `middleware/auth.global.ts` (global) |
|
||||
| `redirect('/login')` | `return navigateTo('/login')` |
|
||||
| Plugin without defineNuxtPlugin | Wrap in defineNuxtPlugin() |
|
||||
|
||||
## Middleware Example: Auth
|
||||
|
||||
```ts
|
||||
// middleware/auth.global.ts
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
// Public routes
|
||||
const publicRoutes = ['/', '/login', '/register']
|
||||
if (publicRoutes.includes(to.path)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check auth
|
||||
if (!auth.isAuthenticated) {
|
||||
return navigateTo('/login')
|
||||
}
|
||||
|
||||
// Check role
|
||||
if (to.meta.requiresAdmin && !auth.isAdmin) {
|
||||
return abortNavigation('Access denied')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Plugin Example: API Client
|
||||
|
||||
```ts
|
||||
// plugins/api.ts
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const api = $fetch.create({
|
||||
baseURL: config.public.apiBase,
|
||||
onRequest({ request, options }) {
|
||||
const auth = useAuthStore()
|
||||
if (auth.token) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${auth.token}`
|
||||
}
|
||||
}
|
||||
},
|
||||
onResponseError({ response }) {
|
||||
if (response.status === 401) {
|
||||
navigateTo('/login')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
provide: {
|
||||
api
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Nuxt middleware: https://nuxt.com/docs/guide/directory-structure/middleware
|
||||
- Nuxt plugins: https://nuxt.com/docs/guide/directory-structure/plugins
|
||||
- Route middleware: https://nuxt.com/docs/getting-started/routing#route-middleware
|
||||
162
.claude/skills/nuxt/references/nuxt-components.md
Normal file
162
.claude/skills/nuxt/references/nuxt-components.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Nuxt Built-in Components
|
||||
|
||||
## When to Use
|
||||
|
||||
Working with images, links, or time display in templates. **Always prefer Nuxt components over HTML elements.**
|
||||
|
||||
## Component Preferences
|
||||
|
||||
| HTML Element | Nuxt Component | Why |
|
||||
| ------------ | -------------- | -------------------------------------- |
|
||||
| `<a>` | `<NuxtLink>` | Client-side navigation, prefetching |
|
||||
| `<img>` | `<NuxtImg>` | Optimization, lazy loading, responsive |
|
||||
| `<time>` | `<NuxtTime>` | SSR-safe formatting, localization |
|
||||
|
||||
## NuxtLink
|
||||
|
||||
**ALWAYS use `<NuxtLink>` instead of `<a>` for internal links:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- Internal navigation -->
|
||||
<NuxtLink to="/about">About</NuxtLink>
|
||||
<NuxtLink :to="{ name: '/users/[userId]', params: { userId } }">Profile</NuxtLink>
|
||||
|
||||
<!-- External links (uses target="_blank" automatically with external) -->
|
||||
<NuxtLink to="https://nuxt.com" external>Nuxt Docs</NuxtLink>
|
||||
|
||||
<!-- Prefetch control -->
|
||||
<NuxtLink to="/dashboard" :prefetch="false">Dashboard</NuxtLink>
|
||||
|
||||
<!-- Active state styling -->
|
||||
<NuxtLink to="/settings" active-class="text-primary" exact-active-class="font-bold">
|
||||
Settings
|
||||
</NuxtLink>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
|
||||
- `to` - Route path or route object
|
||||
- `external` - Force external link behavior
|
||||
- `target` - Link target (`_blank`, etc.)
|
||||
- `prefetch` - Enable/disable prefetching (default: true)
|
||||
- `noPrefetch` - Disable prefetching
|
||||
- `activeClass` - Class when route matches
|
||||
- `exactActiveClass` - Class when route exactly matches
|
||||
|
||||
**Docs:** https://nuxt.com/docs/api/components/nuxt-link
|
||||
|
||||
## NuxtImg
|
||||
|
||||
**ALWAYS use `<NuxtImg>` instead of `<img>` for images:**
|
||||
|
||||
Requires `@nuxt/image` module (usually pre-installed).
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- Basic usage -->
|
||||
<NuxtImg src="/images/hero.jpg" alt="Hero image" />
|
||||
|
||||
<!-- Responsive with sizes -->
|
||||
<NuxtImg
|
||||
src="/images/banner.jpg"
|
||||
alt="Banner"
|
||||
width="1200"
|
||||
height="600"
|
||||
sizes="100vw sm:50vw md:400px"
|
||||
/>
|
||||
|
||||
<!-- Lazy loading (default) -->
|
||||
<NuxtImg src="/images/photo.jpg" loading="lazy" alt="Photo" />
|
||||
|
||||
<!-- Eager loading for above-fold -->
|
||||
<NuxtImg src="/images/logo.svg" loading="eager" alt="Logo" />
|
||||
|
||||
<!-- With placeholder blur -->
|
||||
<NuxtImg src="/images/product.jpg" placeholder alt="Product" />
|
||||
|
||||
<!-- Provider-specific (Cloudinary, etc.) -->
|
||||
<NuxtImg provider="cloudinary" src="/folder/image.jpg" width="500" />
|
||||
|
||||
<!-- Format conversion -->
|
||||
<NuxtImg src="/images/photo.png" format="webp" alt="Photo" />
|
||||
</template>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
|
||||
- `src` - Image source path
|
||||
- `alt` - Alt text (required for accessibility)
|
||||
- `width` / `height` - Dimensions
|
||||
- `sizes` - Responsive sizes
|
||||
- `loading` - `lazy` (default) or `eager`
|
||||
- `placeholder` - Show blur placeholder while loading
|
||||
- `format` - Force output format (`webp`, `avif`, etc.)
|
||||
- `quality` - Image quality (1-100)
|
||||
- `provider` - Image provider (cloudinary, imgix, etc.)
|
||||
|
||||
**For art direction, use `<NuxtPicture>` (different sources per breakpoint).**
|
||||
|
||||
**Docs:** https://image.nuxt.com/usage/nuxt-img
|
||||
|
||||
## NuxtTime
|
||||
|
||||
**ALWAYS use `<NuxtTime>` instead of `<time>` or manual formatting:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- Relative time -->
|
||||
<NuxtTime :datetime="post.createdAt" relative />
|
||||
<!-- Output: "2 hours ago" -->
|
||||
|
||||
<!-- Absolute with locale -->
|
||||
<NuxtTime :datetime="event.date" locale="en-US" />
|
||||
|
||||
<!-- Custom format -->
|
||||
<NuxtTime :datetime="date" year="numeric" month="long" day="numeric" />
|
||||
<!-- Output: "December 6, 2025" -->
|
||||
|
||||
<!-- Short format -->
|
||||
<NuxtTime :datetime="date" month="short" day="numeric" />
|
||||
<!-- Output: "Dec 6" -->
|
||||
|
||||
<!-- With time -->
|
||||
<NuxtTime :datetime="date" hour="numeric" minute="2-digit" />
|
||||
</template>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
|
||||
- `datetime` - Date string, Date object, or timestamp
|
||||
- `relative` - Show relative time ("2 hours ago")
|
||||
- `locale` - Locale for formatting
|
||||
- `year`, `month`, `day`, `hour`, `minute`, `second` - Intl.DateTimeFormat options
|
||||
|
||||
**Docs:** https://nuxt.com/docs/api/components/nuxt-time
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| ❌ Wrong | ✅ Right |
|
||||
| ------------------------------------- | ---------------------------------------- |
|
||||
| `<a href="/about">` | `<NuxtLink to="/about">` |
|
||||
| `<img src="/photo.jpg">` | `<NuxtImg src="/photo.jpg" alt="...">` |
|
||||
| `<time>{{ formatDate(date) }}</time>` | `<NuxtTime :datetime="date" />` |
|
||||
| `formatTimeAgo(date)` in template | `<NuxtTime :datetime="date" relative />` |
|
||||
| `new Date().toLocaleDateString()` | `<NuxtTime :datetime="date" />` |
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **NuxtLink for all internal routes** - enables prefetching and client-side navigation
|
||||
- **NuxtImg for all images** - automatic optimization, lazy loading, responsive
|
||||
- **NuxtTime for all dates** - SSR-safe, automatic localization
|
||||
- **Always provide alt text** for images
|
||||
- **Use `loading="eager"`** for above-the-fold images
|
||||
- **Use sizes prop** for responsive images
|
||||
|
||||
## Resources
|
||||
|
||||
- NuxtLink: https://nuxt.com/docs/api/components/nuxt-link
|
||||
- NuxtImg: https://image.nuxt.com/usage/nuxt-img
|
||||
- NuxtPicture: https://image.nuxt.com/usage/nuxt-picture
|
||||
- NuxtTime: https://nuxt.com/docs/api/components/nuxt-time
|
||||
323
.claude/skills/nuxt/references/nuxt-composables.md
Normal file
323
.claude/skills/nuxt/references/nuxt-composables.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# Nuxt Composables & Utilities
|
||||
|
||||
## When to Use
|
||||
|
||||
Working with Nuxt-specific composables, URL handling, navigation, or data fetching.
|
||||
|
||||
## URL & Request Handling
|
||||
|
||||
### useRequestURL()
|
||||
|
||||
**ALWAYS use `useRequestURL()` instead of `window.origin` or `window.location`:**
|
||||
|
||||
```ts
|
||||
// ✅ Correct - works SSR + client
|
||||
const url = useRequestURL()
|
||||
console.log(url.origin) // https://example.com
|
||||
console.log(url.pathname) // /users/123
|
||||
console.log(url.search) // ?tab=profile
|
||||
|
||||
// ❌ Wrong - breaks on SSR, not available server-side
|
||||
const origin = window.origin
|
||||
const path = window.location.pathname
|
||||
```
|
||||
|
||||
**Why:** `window` is undefined during SSR. `useRequestURL()` works everywhere.
|
||||
|
||||
### useRequestURL() Patterns
|
||||
|
||||
```ts
|
||||
// Get full URL
|
||||
const url = useRequestURL()
|
||||
const fullUrl = url.href // https://example.com/users/123?tab=profile
|
||||
|
||||
// Get origin (base URL)
|
||||
const baseUrl = url.origin // https://example.com
|
||||
|
||||
// Get path
|
||||
const path = url.pathname // /users/123
|
||||
|
||||
// Get query params (use useRoute() instead for better typing)
|
||||
const params = url.searchParams
|
||||
const tab = params.get('tab') // 'profile'
|
||||
|
||||
// Build absolute URL
|
||||
const apiUrl = `${url.origin}/api/users`
|
||||
```
|
||||
|
||||
## Navigation Composables
|
||||
|
||||
### navigateTo()
|
||||
|
||||
```ts
|
||||
// Navigate to route
|
||||
await navigateTo('/about')
|
||||
|
||||
// Type-safe navigation
|
||||
await navigateTo({ name: '/users/[userId]', params: { userId: '123' } })
|
||||
|
||||
// External URL
|
||||
await navigateTo('https://nuxt.com', { external: true })
|
||||
|
||||
// Replace history
|
||||
await navigateTo('/login', { replace: true })
|
||||
|
||||
// Open in new tab
|
||||
await navigateTo('/docs', { open: { target: '_blank' } })
|
||||
|
||||
// Server-side redirect
|
||||
return navigateTo('/login') // in middleware or server route
|
||||
```
|
||||
|
||||
### useRouter()
|
||||
|
||||
```ts
|
||||
const router = useRouter()
|
||||
|
||||
// Navigate
|
||||
router.push({ name: '/users/[userId]', params: { userId: '123' } })
|
||||
|
||||
// Go back
|
||||
router.back()
|
||||
|
||||
// Go forward
|
||||
router.forward()
|
||||
|
||||
// Navigation guards
|
||||
router.beforeEach((to, from) => {
|
||||
// Guard logic
|
||||
})
|
||||
```
|
||||
|
||||
### useRoute()
|
||||
|
||||
```ts
|
||||
// Generic route
|
||||
const route = useRoute()
|
||||
|
||||
// Typed route (preferred)
|
||||
const route = useRoute('/users/[userId]')
|
||||
|
||||
// Access params
|
||||
const userId = route.params.userId
|
||||
|
||||
// Access query
|
||||
const tab = route.query.tab
|
||||
|
||||
// Access meta
|
||||
const requiresAuth = route.meta.requiresAuth
|
||||
```
|
||||
|
||||
## Data Fetching
|
||||
|
||||
### useFetch()
|
||||
|
||||
```ts
|
||||
// Basic fetch
|
||||
const { data, error, pending, refresh } = await useFetch('/api/users')
|
||||
|
||||
// With params
|
||||
const { data } = await useFetch('/api/users', {
|
||||
query: { page: 1, limit: 10 }
|
||||
})
|
||||
|
||||
// With key for deduplication
|
||||
const { data } = await useFetch(`/api/users/${userId}`, {
|
||||
key: `user-${userId}`
|
||||
})
|
||||
|
||||
// Lazy fetch (doesn't block navigation)
|
||||
const { data } = await useLazyFetch('/api/users')
|
||||
|
||||
// Watch and refetch
|
||||
const page = ref(1)
|
||||
const { data } = await useFetch('/api/users', {
|
||||
query: { page },
|
||||
watch: [page]
|
||||
})
|
||||
|
||||
// Cancel requests with AbortController signal (Nuxt 4.2+)
|
||||
const controller = new AbortController()
|
||||
const { data } = await useFetch('/api/users', {
|
||||
signal: controller.signal
|
||||
})
|
||||
// Later: controller.abort() to cancel the request
|
||||
|
||||
// Manual cancellation via execute/refresh
|
||||
const { data, execute } = await useFetch('/api/users', { immediate: false })
|
||||
const abortController = new AbortController()
|
||||
await execute({ signal: abortController.signal })
|
||||
// Later: abortController.abort() to cancel
|
||||
```
|
||||
|
||||
### useAsyncData()
|
||||
|
||||
```ts
|
||||
// Custom async logic
|
||||
const { data, error, pending, refresh } = await useAsyncData('users', async () => {
|
||||
const response = await $fetch('/api/users')
|
||||
return response.filter(u => u.active)
|
||||
})
|
||||
|
||||
// Lazy version
|
||||
const { data } = await useLazyAsyncData('users', async () => {
|
||||
return await $fetch('/api/users')
|
||||
})
|
||||
|
||||
// Cancel with AbortController (Nuxt 4.2+)
|
||||
const controller = new AbortController()
|
||||
const { data } = await useAsyncData('users', async () => {
|
||||
return await $fetch('/api/users', { signal: controller.signal })
|
||||
})
|
||||
// Later: controller.abort() to cancel
|
||||
|
||||
// Custom cache logic with getCachedData
|
||||
const { data } = await useAsyncData('users',
|
||||
async () => $fetch('/api/users'),
|
||||
{
|
||||
getCachedData: (key) => {
|
||||
// Return cached data or null/undefined to trigger fetch
|
||||
const cached = useNuxtData(key)
|
||||
return cached.data.value
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Deep reactivity for nested objects
|
||||
// Default is shallow in Nuxt 4 (was deep in Nuxt 3)
|
||||
const { data } = await useAsyncData('user',
|
||||
async () => $fetch('/api/user'),
|
||||
{
|
||||
deep: true // Makes nested properties reactive
|
||||
}
|
||||
)
|
||||
|
||||
// Deduplication strategies (Nuxt 4.2+)
|
||||
const { data } = await useAsyncData('users',
|
||||
async () => $fetch('/api/users'),
|
||||
{
|
||||
dedupe: 'cancel' // Cancel existing requests when new one starts
|
||||
// dedupe: 'defer' // Prevent new requests while one is pending
|
||||
}
|
||||
)
|
||||
|
||||
// Manual cancellation via execute/refresh
|
||||
const { data, execute } = await useAsyncData('users',
|
||||
async ({ signal }) => $fetch('/api/users', { signal }),
|
||||
{ immediate: false }
|
||||
)
|
||||
const abortController = new AbortController()
|
||||
await execute({ signal: abortController.signal })
|
||||
// Later: abortController.abort() to cancel
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### useState()
|
||||
|
||||
```ts
|
||||
// Create shared state
|
||||
const counter = useState('counter', () => 0)
|
||||
|
||||
// Use in components
|
||||
counter.value++
|
||||
|
||||
// With type
|
||||
const user = useState<User | null>('user', () => null)
|
||||
```
|
||||
|
||||
## App Context
|
||||
|
||||
### useNuxtApp()
|
||||
|
||||
```ts
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
// Access provided values
|
||||
const { $api, $hello } = nuxtApp
|
||||
|
||||
// Access hooks
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
console.log('Page loaded')
|
||||
})
|
||||
|
||||
// Access Vue app
|
||||
nuxtApp.vueApp.use(SomePlugin)
|
||||
```
|
||||
|
||||
### useRuntimeConfig()
|
||||
|
||||
```ts
|
||||
// Access runtime config
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
// Public config (client + server)
|
||||
const apiBase = config.public.apiBase
|
||||
|
||||
// Private config (server only)
|
||||
const apiSecret = config.apiSecret // undefined on client
|
||||
```
|
||||
|
||||
## Head Management
|
||||
|
||||
### useHead()
|
||||
|
||||
```ts
|
||||
// Set page meta
|
||||
useHead({
|
||||
title: 'User Profile',
|
||||
meta: [
|
||||
{ name: 'description', content: 'View user profile' },
|
||||
{ property: 'og:title', content: 'User Profile' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'canonical', href: 'https://example.com/profile' }
|
||||
]
|
||||
})
|
||||
|
||||
// Dynamic values
|
||||
const user = ref({ name: 'John' })
|
||||
useHead({
|
||||
title: () => `${user.value.name}'s Profile`
|
||||
})
|
||||
```
|
||||
|
||||
### useSeoMeta()
|
||||
|
||||
```ts
|
||||
// Cleaner SEO meta
|
||||
useSeoMeta({
|
||||
title: 'User Profile',
|
||||
description: 'View user profile',
|
||||
ogTitle: 'User Profile',
|
||||
ogDescription: 'View user profile',
|
||||
ogImage: 'https://example.com/image.jpg',
|
||||
twitterCard: 'summary_large_image'
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use useRequestURL()** NOT window.origin/location
|
||||
- **Type routes** with useRoute('/path/[param]')
|
||||
- **Use useFetch** for API calls (deduplication, SSR)
|
||||
- **Key your fetches** for proper caching
|
||||
- **useState for shared state** across components
|
||||
- **useSeoMeta** for cleaner SEO tags
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| ❌ Wrong | ✅ Right |
|
||||
| ---------------------------- | ----------------------------------------------------- |
|
||||
| `window.origin` | `useRequestURL().origin` |
|
||||
| `window.location.pathname` | `useRequestURL().pathname` |
|
||||
| `fetch()` in components | `useFetch()` or `useAsyncData()` |
|
||||
| `router.push('/path/' + id)` | `router.push({ name: '/path/[id]', params: { id } })` |
|
||||
| Duplicate fetches | Use `key` parameter |
|
||||
|
||||
## Resources
|
||||
|
||||
- Nuxt composables: https://nuxt.com/docs/api/composables/use-fetch
|
||||
- Data fetching: https://nuxt.com/docs/getting-started/data-fetching
|
||||
- useRequestURL: https://nuxt.com/docs/api/composables/use-request-url
|
||||
- **For NuxtTime, NuxtLink, NuxtImg:** See nuxt-components.md
|
||||
419
.claude/skills/nuxt/references/nuxt-config.md
Normal file
419
.claude/skills/nuxt/references/nuxt-config.md
Normal file
@@ -0,0 +1,419 @@
|
||||
# Nuxt Configuration
|
||||
|
||||
## When to Use
|
||||
|
||||
Configuring `nuxt.config.ts`, modules, auto-imports, runtime config, layers.
|
||||
|
||||
## Basic Structure
|
||||
|
||||
```ts
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
|
||||
modules: [
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@pinia/nuxt'
|
||||
],
|
||||
|
||||
runtimeConfig: {
|
||||
// Private (server-only)
|
||||
apiSecret: process.env.API_SECRET,
|
||||
|
||||
public: {
|
||||
// Public (client + server)
|
||||
apiBase: process.env.API_BASE || 'http://localhost:3000'
|
||||
}
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
title: 'My App',
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Runtime Config
|
||||
|
||||
Access runtime config in app:
|
||||
|
||||
```ts
|
||||
// Server-side
|
||||
const config = useRuntimeConfig()
|
||||
console.log(config.apiSecret) // Available
|
||||
|
||||
// Client-side
|
||||
const config = useRuntimeConfig()
|
||||
console.log(config.public.apiBase) // Available
|
||||
console.log(config.apiSecret) // undefined (private)
|
||||
```
|
||||
|
||||
### Runtime Config Validation (Recommended)
|
||||
|
||||
Use `nuxt-safe-runtime-config` for type-safe runtime config with build-time validation:
|
||||
|
||||
```bash
|
||||
npx nuxi module add nuxt-safe-runtime-config
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Build-time validation (catches missing env vars early)
|
||||
- Optional runtime validation (validates when server starts)
|
||||
- Auto-generated types (no manual type definitions needed)
|
||||
- No manual env var checks required (schema handles validation)
|
||||
|
||||
**Example with Valibot:**
|
||||
|
||||
```ts
|
||||
import { number, object, optional, string } from 'valibot'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
modules: ['nuxt-safe-runtime-config'],
|
||||
|
||||
runtimeConfig: {
|
||||
databaseUrl: process.env.DATABASE_URL,
|
||||
secretKey: process.env.SECRET_KEY,
|
||||
port: Number.parseInt(process.env.PORT || '3000'),
|
||||
public: {
|
||||
apiBase: process.env.PUBLIC_API_BASE,
|
||||
appName: 'My App',
|
||||
},
|
||||
},
|
||||
|
||||
safeRuntimeConfig: {
|
||||
$schema: object({
|
||||
public: object({
|
||||
apiBase: string(),
|
||||
appName: optional(string()),
|
||||
}),
|
||||
databaseUrl: string(),
|
||||
secretKey: string(),
|
||||
port: optional(number()),
|
||||
}),
|
||||
validateAtRuntime: true, // Optional: validate when server starts
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```ts
|
||||
// Auto-typed from schema - no generics needed
|
||||
const config = useSafeRuntimeConfig()
|
||||
// config.public.apiBase is string
|
||||
// config.databaseUrl is string
|
||||
```
|
||||
|
||||
**No manual env checks needed:**
|
||||
|
||||
```ts
|
||||
// ❌ Don't do this with nuxt-safe-runtime-config
|
||||
if (!config.databaseUrl) throw new Error('Missing DATABASE_URL')
|
||||
|
||||
// ✅ Schema validation handles it automatically
|
||||
// If env var is missing, build fails with detailed error
|
||||
```
|
||||
|
||||
Works with Zod, ArkType, or any Standard Schema library. See: https://github.com/onmax/nuxt-safe-runtime-config
|
||||
|
||||
## Auto-Imports
|
||||
|
||||
Nuxt auto-imports from these directories:
|
||||
|
||||
- `components/` - Vue components
|
||||
- `composables/` - Composition functions
|
||||
- `utils/` - Utility functions
|
||||
- `server/utils/` - Server utilities (server-only)
|
||||
|
||||
### Custom Auto-Imports
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
imports: {
|
||||
dirs: [
|
||||
'stores',
|
||||
'types'
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Disable Auto-Import
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
imports: {
|
||||
autoImport: false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@pinia/nuxt',
|
||||
'@vueuse/nuxt',
|
||||
['@nuxtjs/google-fonts', {
|
||||
families: {
|
||||
Inter: [400, 700]
|
||||
}
|
||||
}]
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
## App Config
|
||||
|
||||
For non-sensitive config exposed to client:
|
||||
|
||||
```ts
|
||||
// app.config.ts
|
||||
export default defineAppConfig({
|
||||
theme: {
|
||||
primaryColor: '#3b82f6',
|
||||
borderRadius: '0.5rem'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Access in app:
|
||||
|
||||
```ts
|
||||
const appConfig = useAppConfig()
|
||||
console.log(appConfig.theme.primaryColor)
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
typescript: {
|
||||
strict: true,
|
||||
typeCheck: true,
|
||||
shim: false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Build Configuration
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
build: {
|
||||
transpile: ['some-package']
|
||||
},
|
||||
|
||||
vite: {
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@use "@/assets/styles/variables" as *;'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Route Rules
|
||||
|
||||
Pre-render, cache, or customize routes:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
routeRules: {
|
||||
'/': { prerender: true },
|
||||
'/api/**': { cors: true },
|
||||
'/admin/**': { ssr: false },
|
||||
'/blog/**': { swr: 3600 } // Cache for 1 hour
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### ISR Route Rules
|
||||
|
||||
Use `isr` for incremental static regeneration:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
routeRules: {
|
||||
'/': { prerender: true }, // Static at build time
|
||||
'/**': { isr: 60 }, // Regenerate every 60s
|
||||
'/package/**': { isr: 60 }, // ISR for dynamic routes
|
||||
'/search': { isr: false, cache: false }, // No cache
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Route Rule Layouts (Nuxt 4.3+)
|
||||
|
||||
Apply layouts via route rules for centralized layout management:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
routeRules: {
|
||||
'/admin/**': { appLayout: 'admin' },
|
||||
'/docs/**': { appLayout: 'docs' },
|
||||
'/': { appLayout: 'default' }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Benefits:** Centralized layout control, no need for `setPageLayout()` in every page.
|
||||
|
||||
## Inline Modules
|
||||
|
||||
Add conditional logic during nuxt prepare:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
// Inline function module
|
||||
function (_, nuxt) {
|
||||
if (nuxt.options._prepare) {
|
||||
// Disable expensive operations during prepare
|
||||
nuxt.options.pwa ||= {}
|
||||
nuxt.options.pwa.pwaAssets ||= { disabled: true }
|
||||
}
|
||||
},
|
||||
'@nuxtjs/tailwindcss',
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
## Provider-Specific Modules
|
||||
|
||||
Use `std-env` to detect platform and configure accordingly:
|
||||
|
||||
```ts
|
||||
// modules/vercel-cache.ts
|
||||
import { defineNuxtModule } from 'nuxt/kit'
|
||||
import { provider } from 'std-env'
|
||||
|
||||
export default defineNuxtModule({
|
||||
meta: { name: 'vercel-cache' },
|
||||
setup(_, nuxt) {
|
||||
if (provider !== 'vercel') return
|
||||
|
||||
nuxt.hook('nitro:config', (nitroConfig) => {
|
||||
nitroConfig.storage ||= {}
|
||||
nitroConfig.storage.cache = {
|
||||
driver: 'vercel-runtime-cache',
|
||||
...nitroConfig.storage.cache,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Then register in nuxt.config.ts:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
modules: ['~/modules/vercel-cache']
|
||||
})
|
||||
```
|
||||
|
||||
## Experimental Features
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
future: {
|
||||
compatibilityVersion: 4
|
||||
},
|
||||
|
||||
experimental: {
|
||||
typedPages: true,
|
||||
viewTransition: true,
|
||||
payloadExtraction: true // Enable ISR/SWR payload extraction (Nuxt 4.3+)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Payload extraction** (Nuxt 4.3+): Enables cached payloads during client navigation for ISR/SWR routes, improving performance.
|
||||
|
||||
## Nitro Config
|
||||
|
||||
Server engine configuration:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
nitro: {
|
||||
preset: 'vercel',
|
||||
compressPublicAssets: true,
|
||||
routeRules: {
|
||||
'/api/**': { cors: true }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Layers
|
||||
|
||||
Extend or share configuration:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
extends: [
|
||||
'./base-layer'
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Use `.env` file:
|
||||
|
||||
```env
|
||||
API_SECRET=secret123
|
||||
API_BASE=https://api.example.com
|
||||
```
|
||||
|
||||
Access via runtimeConfig:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
runtimeConfig: {
|
||||
apiSecret: process.env.API_SECRET,
|
||||
public: {
|
||||
apiBase: process.env.API_BASE
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use nuxt-safe-runtime-config** for runtime config with validation
|
||||
- **Public vs private** - keep secrets in private runtimeConfig
|
||||
- **App config** for non-sensitive client config
|
||||
- **Route rules** for performance (prerender, cache, SWR)
|
||||
- **Auto-imports** for cleaner code
|
||||
- **TypeScript strict mode** for better DX
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| ❌ Wrong | ✅ Right |
|
||||
| -------------------------- | ---------------------------- |
|
||||
| Hardcoded API URLs | Use runtimeConfig.public |
|
||||
| Secrets in app.config | Use runtimeConfig (private) |
|
||||
| Import everything manually | Let Nuxt auto-import |
|
||||
| process.env in client code | Use useRuntimeConfig() |
|
||||
| Manual env var validation | Use nuxt-safe-runtime-config |
|
||||
| if (!config.x) throw error | Schema validation handles it |
|
||||
|
||||
## Resources
|
||||
|
||||
- Nuxt config: https://nuxt.com/docs/api/nuxt-config
|
||||
- Runtime config: https://nuxt.com/docs/guide/going-further/runtime-config
|
||||
- App config: https://nuxt.com/docs/guide/directory-structure/app-config
|
||||
- Modules: https://nuxt.com/modules
|
||||
107
.claude/skills/nuxt/references/project-setup.md
Normal file
107
.claude/skills/nuxt/references/project-setup.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Project Setup
|
||||
|
||||
Standard patterns for new Nuxt projects: CI, ESLint, package scripts.
|
||||
|
||||
## CI Workflow
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: {node-version: 22, cache: pnpm}
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm prepare
|
||||
- run: pnpm lint
|
||||
- run: pnpm typecheck
|
||||
- run: pnpm test # if tests exist
|
||||
```
|
||||
|
||||
**With env vars:**
|
||||
|
||||
```yaml
|
||||
env:
|
||||
DATABASE_URL: postgresql://test:test@localhost:5432/test
|
||||
API_KEY: test
|
||||
```
|
||||
|
||||
## ESLint Config
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
import antfu from '@antfu/eslint-config'
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
|
||||
export default withNuxt(
|
||||
antfu({
|
||||
formatters: true,
|
||||
vue: true,
|
||||
pnpm: true,
|
||||
ignores: ['.eslintcache', 'cache/**', '.claude/**', 'README.md', 'docs/**'],
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
**For monorepos, add:**
|
||||
|
||||
```js
|
||||
ignores: ['apps/web/.nuxt/**', 'packages/**/dist/**']
|
||||
```
|
||||
|
||||
## Package Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
"build": "nuxt build",
|
||||
"preview": "nuxt preview",
|
||||
"prepare": "nuxt prepare",
|
||||
"lint": "eslint . --cache",
|
||||
"lint:fix": "eslint . --fix --cache",
|
||||
"typecheck": "nuxt typecheck"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Conventions
|
||||
|
||||
| Convention | Standard |
|
||||
| --------------- | ----------------------------------------------------- |
|
||||
| Package manager | pnpm with `--frozen-lockfile` in CI |
|
||||
| Node version | 22-24 |
|
||||
| ESLint base | @antfu/eslint-config |
|
||||
| Formatter | Via ESLint (`formatters: true`), no separate Prettier |
|
||||
| Cache | `--cache` flag on lint scripts |
|
||||
| Prepare step | Required before lint/typecheck in CI |
|
||||
|
||||
## NuxtHub Deployment
|
||||
|
||||
```yaml
|
||||
# .github/workflows/nuxthub.yml
|
||||
name: Deploy to NuxtHub
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {contents: read, id-token: write}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: {node-version: 22, cache: pnpm}
|
||||
- run: pnpm install
|
||||
- uses: nuxt-hub/action@v2
|
||||
with:
|
||||
project-key: your-project-key
|
||||
```
|
||||
|
||||
> **For pnpm catalogs, release workflows, tsconfig patterns:** see `ts-library` skill
|
||||
242
.claude/skills/nuxt/references/routing.md
Normal file
242
.claude/skills/nuxt/references/routing.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Nuxt File-Based Routing
|
||||
|
||||
## When to Use
|
||||
|
||||
Working with `pages/` or `layouts/` directories, file-based routing, navigation.
|
||||
|
||||
## File-Based Routing Basics
|
||||
|
||||
`pages/` folder structure directly maps to routes. File names determine URLs.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
**Key principles:**
|
||||
|
||||
- **ALWAYS use descriptive params:** `[userId].vue` NOT `[id].vue`
|
||||
- **Optional params:** `[[paramName]].vue`
|
||||
- **Catch-all:** `[...path].vue`
|
||||
- **Route groups for organization:** `(folder)/` groups files without affecting URLs
|
||||
|
||||
## Red Flags - Stop and Check Skill
|
||||
|
||||
If you're thinking any of these, STOP and re-read this skill:
|
||||
|
||||
- "String paths are simpler than typed routes"
|
||||
- "Generic param names like [id] are fine"
|
||||
- "I remember how Nuxt 3 worked"
|
||||
|
||||
All of these mean: You're about to use outdated patterns. Use Nuxt 4 patterns instead.
|
||||
|
||||
## File Structure Example
|
||||
|
||||
```
|
||||
pages/
|
||||
├── index.vue # /
|
||||
├── about.vue # /about
|
||||
├── [...slug].vue # catch-all for 404
|
||||
├── users.vue # parent route (layout for /users/*)
|
||||
└── users/
|
||||
├── index.vue # /users
|
||||
└── [userId].vue # /users/:userId
|
||||
```
|
||||
|
||||
## Route Groups for Organization
|
||||
|
||||
Route groups organize files WITHOUT affecting URLs. Wrap folder names in parentheses:
|
||||
|
||||
```
|
||||
pages/
|
||||
├── (marketing)/ # group folder (ignored in URL)
|
||||
│ ├── about.vue # /about (not /marketing/about)
|
||||
│ └── pricing.vue # /pricing
|
||||
└── (admin)/ # group folder (ignored in URL)
|
||||
├── dashboard.vue # /dashboard
|
||||
└── settings.vue # /settings
|
||||
```
|
||||
|
||||
**Use route groups to:**
|
||||
|
||||
- Organize pages by feature/team
|
||||
- Group related routes without affecting URLs
|
||||
- Keep large projects maintainable
|
||||
- Apply middleware to specific groups (via `route.meta.groups`)
|
||||
|
||||
**Access route groups in middleware:**
|
||||
|
||||
```ts
|
||||
// middleware/auth.global.ts
|
||||
export default defineNuxtRouteMiddleware((to) => {
|
||||
// Check if route is in admin group
|
||||
if (to.meta.groups?.includes('admin')) {
|
||||
const auth = useAuthStore()
|
||||
if (!auth.isAdmin) return navigateTo('/')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Parent Routes (Layouts)
|
||||
|
||||
Parent route = layout for nested routes:
|
||||
|
||||
```vue
|
||||
<!-- pages/users.vue -->
|
||||
<template>
|
||||
<div class="users-layout">
|
||||
<nav>
|
||||
<NuxtLink to="/users">All Users</NuxtLink>
|
||||
<NuxtLink to="/users/create">Create User</NuxtLink>
|
||||
</nav>
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Child routes:
|
||||
|
||||
```
|
||||
pages/
|
||||
├── users.vue # Parent route with <NuxtPage />
|
||||
└── users/
|
||||
├── index.vue # /users
|
||||
├── [userId].vue # /users/:userId
|
||||
└── create.vue # /users/create
|
||||
```
|
||||
|
||||
## definePage() for Route Customization
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'user-profile',
|
||||
path: '/profile/:userId', // Override default path
|
||||
alias: ['/me', '/profile'],
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: 'User Profile',
|
||||
roles: ['user', 'admin']
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>Profile content</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Typed Router
|
||||
|
||||
**ALWAYS use typed routes for navigation:**
|
||||
|
||||
```ts
|
||||
// ✅ Type-safe with route name
|
||||
await navigateTo({ name: '/users/[userId]', params: { userId: '123' } })
|
||||
|
||||
// ❌ String-based (not type-safe, avoid)
|
||||
await navigateTo('/users/123')
|
||||
```
|
||||
|
||||
**REQUIRED: Check `typed-router.d.ts` for available route names and params before navigating.**
|
||||
|
||||
## useRoute with Types
|
||||
|
||||
Pass route name for stricter typing:
|
||||
|
||||
```ts
|
||||
// Generic route
|
||||
const route = useRoute()
|
||||
|
||||
// Typed route (preferred)
|
||||
const route = useRoute('/users/[userId]')
|
||||
// route.params.userId is now typed correctly
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
```ts
|
||||
// Navigate to route
|
||||
await navigateTo('/about')
|
||||
await navigateTo({ name: '/users/[userId]', params: { userId: '123' } })
|
||||
|
||||
// Navigate with query
|
||||
await navigateTo({ path: '/search', query: { q: 'nuxt' } })
|
||||
|
||||
// External redirect
|
||||
await navigateTo('https://nuxt.com', { external: true })
|
||||
|
||||
// Replace history
|
||||
await navigateTo('/login', { replace: true })
|
||||
|
||||
// Open in new tab
|
||||
await navigateTo('/docs', { open: { target: '_blank' } })
|
||||
```
|
||||
|
||||
## Route Meta & Middleware
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['auth', 'admin'],
|
||||
layout: 'dashboard',
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Dynamic Layout Switching
|
||||
|
||||
Use `setPageLayout()` to switch layouts programmatically:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const user = useUser()
|
||||
|
||||
// Switch layout based on auth state
|
||||
if (!user.value) {
|
||||
setPageLayout('guest')
|
||||
} else {
|
||||
setPageLayout('dashboard')
|
||||
}
|
||||
|
||||
// With layout props (Nuxt 4.3+)
|
||||
setPageLayout('dashboard', {
|
||||
sidebar: 'collapsed',
|
||||
theme: 'dark'
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Dynamic Routes Patterns
|
||||
|
||||
```
|
||||
[userId].vue # /users/123
|
||||
[[slug]].vue # /blog or /blog/post (optional)
|
||||
[...path].vue # /a/b/c (catch-all)
|
||||
[[...path]].vue # / or /a/b/c (optional catch-all)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **`index.vue` for index routes** - valid and correct for creating default routes
|
||||
- **Route groups `(folder)/` for organization** - group files without affecting URLs
|
||||
- **Descriptive param names** - `[userId]` not `[id]`, `[postSlug]` not `[slug]`
|
||||
- **Type-safe navigation** - use route names, not strings
|
||||
- **Check typed-router.d.ts** for available routes
|
||||
- **Parent routes for layouts** - `users.vue` with `<NuxtPage />`
|
||||
- **Use definePage** for custom paths/aliases
|
||||
- **Catch-all for 404** - `[...path].vue` or `[...slug].vue`
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| ❌ Wrong | ✅ Right |
|
||||
| ---------------------------- | ----------------------------------------------------------------- |
|
||||
| `[id].vue` | `[userId].vue` or `[postId].vue` |
|
||||
| `navigateTo('/users/' + id)` | `navigateTo({ name: '/users/[userId]', params: { userId: id } })` |
|
||||
| `<Nuxt />` | `<NuxtPage />` |
|
||||
| Separate layouts/ folder | Parent routes with `<NuxtPage />` |
|
||||
|
||||
## Resources
|
||||
|
||||
- Nuxt routing: https://nuxt.com/docs/guide/directory-structure/pages
|
||||
- File-based routing: https://nuxt.com/docs/getting-started/routing
|
||||
451
.claude/skills/nuxt/references/server.md
Normal file
451
.claude/skills/nuxt/references/server.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# Nuxt Server Patterns
|
||||
|
||||
> **Versions:** Nuxt uses h3 v1 and nitropack v2. Patterns from h3 v2 or nitro v3 docs won't work.
|
||||
|
||||
## When to Use
|
||||
|
||||
Working with `server/` directory - API routes, server middleware, server utilities.
|
||||
|
||||
## Server Directory Structure
|
||||
|
||||
```
|
||||
server/
|
||||
├── api/ # API endpoints
|
||||
│ ├── users.get.ts # GET /api/users
|
||||
│ ├── users.post.ts # POST /api/users
|
||||
│ └── users/
|
||||
│ └── [id].get.ts # GET /api/users/:id
|
||||
├── routes/ # Non-API routes
|
||||
│ └── healthz.get.ts # GET /healthz
|
||||
├── middleware/ # Server middleware
|
||||
│ └── log.ts
|
||||
└── utils/ # Server utilities (auto-imported)
|
||||
└── db.ts
|
||||
```
|
||||
|
||||
## API Routes
|
||||
|
||||
File naming determines HTTP method and route:
|
||||
|
||||
- `users.get.ts` → GET /api/users
|
||||
- `users.post.ts` → POST /api/users
|
||||
- `users/[userId].get.ts` → GET /api/users/:userId
|
||||
- `users/[userId].delete.ts` → DELETE /api/users/:userId
|
||||
|
||||
**REQUIRED: Use descriptive param names:** `[userId].get.ts` NOT `[id].get.ts`
|
||||
|
||||
## Red Flags - Stop and Check Skill
|
||||
|
||||
If you're thinking any of these, STOP and re-read this skill:
|
||||
|
||||
- "I'll use event.context.params like before"
|
||||
- "Generic [id] is fine for params"
|
||||
- "Don't need .get.ts suffix"
|
||||
- "I remember how Nuxt 3 API routes worked"
|
||||
|
||||
All of these mean: You're using outdated patterns. Use Nuxt 4 patterns instead.
|
||||
|
||||
### Basic API Route
|
||||
|
||||
```ts
|
||||
// server/api/users.get.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const users = await fetchUsers()
|
||||
return users
|
||||
})
|
||||
```
|
||||
|
||||
### Route with Params
|
||||
|
||||
```ts
|
||||
// server/api/users/[userId].get.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const userId = getRouterParam(event, 'userId')
|
||||
|
||||
if (!userId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'User ID is required'
|
||||
})
|
||||
}
|
||||
|
||||
const user = await fetchUserById(userId)
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: 'User not found'
|
||||
})
|
||||
}
|
||||
|
||||
return user
|
||||
})
|
||||
```
|
||||
|
||||
### Route with Query Params
|
||||
|
||||
```ts
|
||||
// server/api/users.get.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event)
|
||||
const page = Number(query.page) || 1
|
||||
const limit = Number(query.limit) || 10
|
||||
|
||||
const users = await fetchUsers({ page, limit })
|
||||
return users
|
||||
})
|
||||
```
|
||||
|
||||
### Route with Body
|
||||
|
||||
```ts
|
||||
// server/api/users.post.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate body
|
||||
if (!body.name || !body.email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'Missing required fields: name, email'
|
||||
})
|
||||
}
|
||||
|
||||
const user = await createUser(body)
|
||||
setResponseStatus(event, 201)
|
||||
return user
|
||||
})
|
||||
```
|
||||
|
||||
### Validation with Valibot
|
||||
|
||||
Use `readValidatedBody` and `getValidatedQuery` for schema validation:
|
||||
|
||||
```ts
|
||||
// server/api/users.post.ts
|
||||
import * as v from 'valibot'
|
||||
|
||||
const UserSchema = v.object({
|
||||
name: v.pipe(v.string(), v.minLength(1)),
|
||||
email: v.pipe(v.string(), v.email())
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readValidatedBody(event, v.parser(UserSchema))
|
||||
// body is typed as { name: string, email: string }
|
||||
const user = await createUser(body)
|
||||
setResponseStatus(event, 201)
|
||||
return user
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// server/api/users.get.ts
|
||||
import * as v from 'valibot'
|
||||
|
||||
const QuerySchema = v.object({
|
||||
page: v.optional(v.pipe(v.string(), v.transform(Number)), '1'),
|
||||
limit: v.optional(v.pipe(v.string(), v.transform(Number)), '10')
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { page, limit } = await getValidatedQuery(event, v.parser(QuerySchema))
|
||||
return fetchUsers({ page, limit })
|
||||
})
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Use `createError` for HTTP errors:
|
||||
|
||||
```ts
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Bad Request',
|
||||
message: 'Invalid input',
|
||||
data: { field: 'email' } // Optional additional data
|
||||
})
|
||||
```
|
||||
|
||||
## Server Middleware
|
||||
|
||||
Runs on every server request:
|
||||
|
||||
```ts
|
||||
// server/middleware/log.ts
|
||||
export default defineEventHandler((event) => {
|
||||
console.log(`${event.method} ${event.path}`)
|
||||
})
|
||||
```
|
||||
|
||||
Named middleware for specific patterns:
|
||||
|
||||
```ts
|
||||
// server/middleware/auth.ts
|
||||
export default defineEventHandler((event) => {
|
||||
const token = getRequestHeader(event, 'authorization')
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized'
|
||||
})
|
||||
}
|
||||
|
||||
// Attach user to event context
|
||||
event.context.user = await verifyToken(token)
|
||||
})
|
||||
```
|
||||
|
||||
## Server Utils
|
||||
|
||||
Reusable server functions (auto-imported):
|
||||
|
||||
```ts
|
||||
// server/utils/db.ts
|
||||
import { db } from './database'
|
||||
|
||||
export async function fetchUsers(options: { page: number, limit: number }) {
|
||||
return await db.select().from('users').limit(options.limit).offset((options.page - 1) * options.limit)
|
||||
}
|
||||
|
||||
export async function fetchUserById(id: string) {
|
||||
return await db.select().from('users').where({ id }).first()
|
||||
}
|
||||
```
|
||||
|
||||
Auto-imported in all server routes and middleware.
|
||||
|
||||
**Import server utils from client (Nuxt 4.3+):**
|
||||
|
||||
```ts
|
||||
// Use #server alias for type-safe server-only imports
|
||||
import type { User } from '#server/utils/db'
|
||||
```
|
||||
|
||||
**Note:** Only types are imported; actual server code never bundles into client.
|
||||
|
||||
## Cached Functions
|
||||
|
||||
Use `defineCachedFunction` for caching expensive operations in server utils:
|
||||
|
||||
```ts
|
||||
// server/utils/github.ts
|
||||
export const fetchRepo = defineCachedFunction(
|
||||
async (owner: string, repo: string) => {
|
||||
return await $fetch(`https://api.github.com/repos/${owner}/${repo}`)
|
||||
},
|
||||
{
|
||||
maxAge: 60 * 5, // Cache for 5 minutes
|
||||
swr: true, // Stale-while-revalidate
|
||||
name: 'github-repo',
|
||||
getKey: (owner, repo) => `${owner}/${repo}`,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Cached Event Handlers
|
||||
|
||||
Use `defineCachedEventHandler` for ISR-style caching on API routes:
|
||||
|
||||
```ts
|
||||
// server/api/products/[productId].get.ts
|
||||
export default defineCachedEventHandler(
|
||||
async (event) => {
|
||||
const productId = getRouterParam(event, 'productId')
|
||||
return await fetchProductById(productId)
|
||||
},
|
||||
{
|
||||
maxAge: 3600, // Cache for 1 hour
|
||||
swr: true, // Serve stale while revalidating
|
||||
getKey: event => getRouterParam(event, 'productId') ?? '',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Generic Error Handler
|
||||
|
||||
Centralize error handling for H3 errors, validation errors, and fallbacks:
|
||||
|
||||
```ts
|
||||
// server/utils/error-handler.ts
|
||||
import { isError, createError } from 'h3'
|
||||
import * as v from 'valibot'
|
||||
|
||||
export function handleApiError(error: unknown, fallback: { statusCode?: number, message: string }): never {
|
||||
// Re-throw existing H3 errors
|
||||
if (isError(error)) throw error
|
||||
|
||||
// Handle Valibot validation errors
|
||||
if (v.isValiError(error)) {
|
||||
throw createError({ statusCode: 400, message: error.issues[0].message })
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
throw createError({ statusCode: fallback.statusCode ?? 502, message: fallback.message })
|
||||
}
|
||||
```
|
||||
|
||||
Usage in routes:
|
||||
|
||||
```ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const data = await fetchExternalApi()
|
||||
return data
|
||||
} catch (error) {
|
||||
handleApiError(error, { statusCode: 502, message: 'Failed to fetch data' })
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Request Helpers
|
||||
|
||||
```ts
|
||||
// Get params
|
||||
const userId = getRouterParam(event, 'userId')
|
||||
|
||||
// Get query
|
||||
const query = getQuery(event)
|
||||
|
||||
// Get body
|
||||
const body = await readBody(event)
|
||||
|
||||
// Get headers
|
||||
const auth = getRequestHeader(event, 'authorization')
|
||||
|
||||
// Get cookies
|
||||
const token = getCookie(event, 'token')
|
||||
|
||||
// Get method
|
||||
const method = getMethod(event)
|
||||
|
||||
// Get IP
|
||||
const ip = getRequestIP(event)
|
||||
```
|
||||
|
||||
## Response Helpers
|
||||
|
||||
```ts
|
||||
// Set status code
|
||||
setResponseStatus(event, 201)
|
||||
|
||||
// Set headers
|
||||
setResponseHeader(event, 'X-Custom', 'value')
|
||||
setResponseHeaders(event, { 'X-Custom': 'value', 'X-Another': 'value' })
|
||||
|
||||
// Set cookies
|
||||
setCookie(event, 'token', 'value', {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7 // 1 week
|
||||
})
|
||||
|
||||
// Redirect
|
||||
return sendRedirect(event, '/login', 302)
|
||||
|
||||
// Stream
|
||||
return sendStream(event, stream)
|
||||
|
||||
// No content
|
||||
return sendNoContent(event)
|
||||
```
|
||||
|
||||
## Background Tasks
|
||||
|
||||
Use `event.waitUntil()` for async tasks that shouldn't block the response (Nuxt 4+):
|
||||
|
||||
```ts
|
||||
// server/api/analytics.post.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const data = await readBody(event)
|
||||
|
||||
// Don't block response with analytics logging
|
||||
event.waitUntil(
|
||||
logAnalytics(data)
|
||||
)
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
```
|
||||
|
||||
**Use cases:** logging, caching, background processing, async cleanup.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use descriptive param names** - `[userId]` not `[id]`
|
||||
- **Keep routes thin** - delegate to server utils
|
||||
- **Validate input** at route level
|
||||
- **Use typed errors** with createError
|
||||
- **Handle errors gracefully** - don't expose internals
|
||||
- **Use server utils** for DB/external APIs
|
||||
- **Don't expose sensitive data** in responses
|
||||
- **Set proper status codes** - 201 for created, 204 for no content
|
||||
- **Use event.waitUntil()** for background tasks that shouldn't block responses
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| ❌ Wrong | ✅ Right |
|
||||
| ------------------------- | ----------------------------- |
|
||||
| `event.context.params.id` | `getRouterParam(event, 'id')` |
|
||||
| `return res.json(data)` | `return data` |
|
||||
| `[id].get.ts` | `[userId].get.ts` |
|
||||
| `users-id.get.ts` | `users/[id].get.ts` |
|
||||
| Throw generic errors | Use createError with status |
|
||||
|
||||
## WebSocket
|
||||
|
||||
```ts
|
||||
// server/routes/_ws.ts
|
||||
export default defineWebSocketHandler({
|
||||
open(peer) {
|
||||
console.log('Client connected:', peer.id)
|
||||
},
|
||||
message(peer, message) {
|
||||
peer.send(`Echo: ${message.text()}`)
|
||||
// Broadcast to all: peer.publish('channel', message)
|
||||
},
|
||||
close(peer) {
|
||||
console.log('Client disconnected:', peer.id)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Enable in config:
|
||||
|
||||
```ts
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
nitro: {
|
||||
experimental: { websocket: true }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Server-Sent Events (Experimental)
|
||||
|
||||
```ts
|
||||
// server/api/stream.get.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const stream = createEventStream(event)
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
await stream.push({ data: JSON.stringify({ time: Date.now() }) })
|
||||
}, 1000)
|
||||
|
||||
stream.onClosed(() => {
|
||||
clearInterval(interval)
|
||||
})
|
||||
|
||||
return stream.send()
|
||||
})
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Nuxt server: https://nuxt.com/docs/guide/directory-structure/server
|
||||
- h3 (Nitro engine): https://v1.h3.dev/
|
||||
- Nitro: https://nitro.build/
|
||||
|
||||
> **For database/storage APIs:** see `nuxthub` skill
|
||||
Reference in New Issue
Block a user