Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:38:06 +08:00
commit 8bd861b848
17 changed files with 6862 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
{
"name": "nuxt",
"description": "Nuxt development guidance with Vue best practices, auto-imports awareness, and library-specific patterns for Pinia, VueUse, and more",
"version": "2.3.0",
"author": {
"name": "Lukas Trumm"
},
"skills": [
"./skills"
],
"commands": [
"./commands"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# nuxt
Nuxt development guidance with Vue best practices, auto-imports awareness, and library-specific patterns for Pinia, VueUse, and more

View File

@@ -0,0 +1,488 @@
---
description: Load Vue component patterns and best practices
---
# Vue Component Priming
> **Note:** This command references the `nuxt:nuxt` skill for progressive disclosure of additional Vue patterns and library-specific documentation.
## Script Setup Syntax
ALWAYS use `<script setup lang="ts">` for component script sections.
### Props
ALWAYS use TypeScript type-based syntax for `defineProps()`:
```typescript
// ✅ Correct: Type-based with destructuring and inline defaults
const {
title,
count = 0,
enabled = true,
} = defineProps<{
title: string
count?: number
enabled?: boolean
}>()
// ✅ Correct: No props used in script
defineProps<{
title: string
}>()
// ❌ Wrong: Runtime PropType syntax
import type { PropType } from "vue"
defineProps({
items: {
type: Array as PropType<string[]>,
required: true,
},
})
```
### Emits
ALWAYS use type-based syntax for `defineEmits`:
```typescript
// ✅ Correct: Type-based emits
const emit = defineEmits<{
update: [value: string]
close: []
}>()
// ❌ Wrong: Runtime array syntax
const emit = defineEmits(["update", "close"])
```
### Event Handler Typing
When emitting events with event objects, use appropriate event types:
```typescript
// ✅ Correct: Typed event handlers
const emit = defineEmits<{
click: [event: MouseEvent]
keypress: [event: KeyboardEvent]
input: [event: InputEvent]
submit: [event: SubmitEvent]
}>()
// Usage in template
<button @click="emit('click', $event)">Click me</button>
<input @keypress="emit('keypress', $event)" />
```
### v-model
USE `defineModel()` for v-model implementations:
```typescript
// ✅ Correct: Using defineModel
const modelValue = defineModel<string>()
// With options
const modelValue = defineModel<string>({ required: true })
// ❌ Wrong: Manual prop + emit
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ "update:modelValue": [value: string] }>()
```
## Component Structure
### Template Placement
ALWAYS place `<template>` section first, before `<script>` and `<style>`:
```vue
<!-- Correct order -->
<template>
<div>{{ title }}</div>
</template>
<script setup lang="ts">
const { title } = defineProps<{ title: string }>()
</script>
<style scoped>
div {
color: blue;
}
</style>
```
### Component Naming
- ALWAYS use multi-word component names except for Nuxt pages and layouts
- Examples: `UserProfile.vue`, `SearchBar.vue` (not `User.vue`, `Search.vue`)
- Exception: `pages/index.vue`, `pages/about.vue`, `layouts/default.vue`
## Template Directives
### v-for Loops
ALWAYS use `key` in v-for loops and prefer `of` over `in`:
```vue
<!-- Correct -->
<li v-for="user of users" :key="user.id">{{ user.name }}</li>
<!-- Wrong: Missing key -->
<li v-for="user of users">{{ user.name }}</li>
<!-- Wrong: Using 'in' instead of 'of' -->
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
```
### Prop Binding Shorthand
ALWAYS use shorthand syntax when passing props with same name as variable:
```vue
<!-- Correct: Shorthand -->
<UserCard :username :avatar :bio />
<!-- Wrong: Verbose when unnecessary -->
<UserCard :username="username" :avatar="avatar" :bio="bio" />
```
## Reactivity and State
### Reactive References
PREFER `ref()` for reactive state instead of `reactive()`:
```typescript
// ✅ Preferred: Using ref
const count = ref(0)
const user = ref({ name: "Alice", age: 30 })
// ❌ Less preferred: Using reactive (loses reactivity on destructure)
const state = reactive({ count: 0 })
```
### Auto-Imported Vue APIs
Never manually import these in Nuxt projects - they're auto-imported:
**Reactivity:**
- `ref` - Reactive primitive values
- `reactive` - Reactive objects
- `computed` - Computed values
- `watch` - Watch reactive values
**Lifecycle:**
- `onMounted` - Component mounted
- `onUnmounted` - Component unmounted
- `onBeforeMount`, `onBeforeUnmount`, etc.
**Component APIs:**
- `defineProps` - Define props (type-based)
- `defineEmits` - Define emits (type-based)
- `defineModel` - Define v-model (type-based)
**Utilities:**
- `useId` - Generate unique IDs for accessibility/form elements (SSR-safe)
## Component Organization
### Logical Grouping
PREFER to group by logical concerns rather than by type:
```typescript
// ✅ Preferred: Grouped by feature/concern
<script setup lang="ts">
// User authentication concern
const user = ref(null)
const isAuthenticated = computed(() => !!user.value)
async function login() { /* ... */ }
// Search functionality concern
const searchQuery = ref('')
const searchResults = computed(() => /* ... */)
function handleSearch() { /* ... */ }
</script>
// ❌ Less preferred: Grouped by type
<script setup lang="ts">
// All refs
const user = ref(null)
const searchQuery = ref('')
// All computed
const isAuthenticated = computed(() => !!user.value)
const searchResults = computed(() => /* ... */)
// All functions
async function login() { /* ... */ }
function handleSearch() { /* ... */ }
</script>
```
## Styling Strategy
**Check `package.json` for `@nuxtjs/tailwindcss`:**
### If Tailwind Installed
```vue
<template>
<div class="flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-800">
<!-- Use Tailwind utilities -->
<!-- Arbitrary variants: [&::-webkit-scrollbar]:w-1.5 -->
<!-- Custom properties: @theme directive -->
</div>
</template>
```
### If NO Tailwind
ALWAYS use `<style scoped>`. PREFER short, simple class names - scoped styles eliminate need for BEM:
```vue
<!-- Preferred -->
<template>
<div class="card">
<div class="header">Title</div>
<div class="content">Body</div>
</div>
</template>
<style scoped>
.card { padding: 1rem; }
.header { font-weight: bold; }
.content { flex: 1; }
</style>
<!-- Avoid: BEM with scoped styles -->
<div class="user-card">
<div class="user-card__header">Title</div>
</div>
```
## VueUse Composables (If Installed)
Check `package.json` for `@vueuse/core` or `@vueuse/nuxt`:
PREFER VueUse composables over custom implementations for common tasks:
```typescript
// ✅ Preferred: Using VueUse (if installed)
import { useLocalStorage, useMouse, useWindowSize } from "@vueuse/core"
const token = useLocalStorage("auth-token", "")
// ❌ Avoid: Custom implementation when VueUse exists
const token = ref(localStorage.getItem("auth-token") || "")
watch(token, (val) => localStorage.setItem("auth-token", val))
```
### Common VueUse Patterns
**State:**
- `useToggle`, `useCounter`, `useLocalStorage`, `useSessionStorage`
**DOM:**
- `useMouse`, `useScroll`, `useElementVisibility`, `useIntersectionObserver`, `useResizeObserver`
**Browser:**
- `useClipboard`, `useMediaQuery`, `useDark`, `usePreferredDark`, `useGeolocation`
**Utilities:**
- `refDebounced`, `useDebounceFn`, `refThrottled`, `useThrottleFn`, `useInterval`, `useTimeout`
The `nuxt:nuxt` skill provides detailed VueUse reference when installed.
## Accessibility
- Use semantic HTML: `<button>`, `<nav>`, `<main>`, `<article>`
- Add ARIA attributes to interactive elements
- Ensure keyboard navigation (tab order, enter/space handlers)
## TypeScript Types
- Place component prop interfaces in same file or `/types` directory
- Use PascalCase: `ButtonProps`, `CardProps`, `UserState`
- Never use `as any` - prefer type guards or `as unknown as Type`
## Performance Patterns
### Computed vs Methods
Use `computed()` for derived state (cached):
```typescript
// ✅ Cached, only recalculates when dependencies change
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// ❌ Recalculates on every render
const getFullName = () => `${firstName.value} ${lastName.value}`
```
### Static Content
Use `v-once` for static content that never changes:
```vue
<div v-once>
<h1>Static Header</h1>
<p>This content never changes</p>
</div>
```
### Expensive Lists
Use `v-memo` for expensive lists with stable data:
```vue
<div v-for="item of list" :key="item.id" v-memo="[item.id]">
<!-- Expensive rendering -->
</div>
```
## Template Best Practices
### Conditional Rendering
- Use `v-show` for frequent toggles
- Use `v-if` for conditional rendering
```vue
<!-- Frequent toggling: keep in DOM -->
<div v-show="isVisible">Toggle me often</div>
<!-- Conditional: add/remove from DOM -->
<div v-if="hasPermission">Render only when needed</div>
```
### Event Handling
```vue
<!-- Inline handlers for simple cases -->
<button @click="count++">Increment</button>
<!-- Method refs for complex logic -->
<button @click="handleSubmit">Submit</button>
<!-- Modifiers -->
<button @click.prevent="handleClick">Prevent Default</button>
<input @keyup.enter="handleEnter" />
```
### Slots
```vue
<!-- Basic slot -->
<template>
<div class="card">
<slot />
</div>
</template>
<!-- Named slots -->
<template>
<div class="card">
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>
</template>
<!-- Scoped slots -->
<template>
<ul>
<li v-for="item of items" :key="item.id">
<slot :item="item" />
</li>
</ul>
</template>
```
## Provide/Inject
For dependency injection:
```typescript
// Parent component
provide("theme", "dark")
provide("api", apiClient)
// Child component (any depth)
const theme = inject("theme")
const api = inject("api")
// With TypeScript
import type { InjectionKey } from "vue"
interface Theme {
mode: "light" | "dark"
}
const themeKey: InjectionKey<Theme> = Symbol("theme")
// Provide
provide(themeKey, { mode: "dark" })
// Inject
const theme = inject(themeKey)
```
## Lifecycle Hooks
```typescript
// Setup (reactive state initialization)
const count = ref(0)
// Mounted (DOM available)
onMounted(() => {
console.log("Component mounted")
})
// Before unmount (cleanup)
onBeforeUnmount(() => {
// Remove event listeners, clear timers, etc.
})
// Unmounted
onUnmounted(() => {
console.log("Component unmounted")
})
// Watch effect (runs immediately and on dependencies change)
watchEffect(() => {
console.log(`Count is ${count.value}`)
})
// Watch specific value
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
```
## Template Refs
```vue
<template>
<input ref="inputRef" />
<MyComponent ref="componentRef" />
</template>
<script setup lang="ts">
const inputRef = ref<HTMLInputElement>()
const componentRef = ref<InstanceType<typeof MyComponent>>()
onMounted(() => {
inputRef.value?.focus()
})
</script>
```

552
commands/prime/framework.md Normal file
View File

@@ -0,0 +1,552 @@
---
description: Load Nuxt framework patterns and conventions
---
# Nuxt Framework Priming
> **Note:** This command references the `nuxt:nuxt` skill for progressive disclosure of additional patterns and module-specific documentation.
## Data Fetching
### useFetch for API Calls
Use `useFetch` for API endpoints. It runs on both server and client, with automatic hydration.
```typescript
const { data, status, error, refresh } = await useFetch("/api/users")
// With query parameters
const { data, status } = await useFetch("/api/users", {
query: { limit: 10, page: 1 },
})
// With type safety
interface User {
id: number
name: string
}
const { data, status, error } = await useFetch<User[]>("/api/users")
```
**Always handle all states in templates:**
```vue
<template>
<div v-if="status === 'pending'">Loading...</div>
<div v-else-if="status === 'error'">
<p>Error: {{ error?.message }}</p>
</div>
<div v-else-if="data">
<!-- Success state -->
<ul>
<li v-for="user of data" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
```
### useAsyncData for Complex Data
Use `useAsyncData` when you need more control or complex transformations.
```typescript
const { data, status, error } = await useAsyncData("users", async () => {
const users = await $fetch("/api/users")
const stats = await $fetch("/api/stats")
return { users, stats }
})
// With caching key
const { data, status, error } = await useAsyncData(`user-${id}`, () =>
$fetch(`/api/users/${id}`),
)
```
### Lazy Fetching
Use lazy variants when you don't want to block navigation:
```typescript
// Non-blocking
const { status, data } = await useLazyFetch('/api/users')
// Show loading state
<div v-if="status === 'pending'">Loading...</div>
<div v-else>{{ data }}</div>
```
### Client-Only Fetching
```typescript
const { data } = await useFetch("/api/users", {
server: false, // Only fetch on client
})
```
### Refresh and Refetch
```typescript
const { data, status, refresh } = await useFetch("/api/users")
// Manually refetch
await refresh()
// Refetch on event
watch(searchQuery, () => refresh())
```
## SEO and Meta Tags
### useHead
```typescript
useHead({
title: "My Page",
meta: [
{ name: "description", content: "Page description" },
{ property: "og:title", content: "My Page" },
],
link: [{ rel: "canonical", href: "https://example.com/page" }],
})
```
### useSeoMeta (Type-Safe)
```typescript
useSeoMeta({
title: "My Page",
description: "Page description",
ogTitle: "My Page",
ogDescription: "Page description",
ogImage: "https://example.com/image.jpg",
twitterCard: "summary_large_image",
})
```
### definePageMeta
```typescript
definePageMeta({
title: "User Profile",
description: "View user profile",
middleware: ["auth"],
})
```
## Error Handling
### Show Error Page
```typescript
showError({
statusCode: 404,
message: "Page not found",
})
// With custom error
showError({
statusCode: 403,
message: "Access denied",
fatal: true,
})
```
### Clear Error
```typescript
clearError({ redirect: "/" })
```
### Handle Errors in Data Fetching
```typescript
const { data, status, error } = await useFetch("/api/users")
if (error.value) {
showError({
statusCode: error.value.statusCode,
message: error.value.message,
})
}
```
### Error Component
```vue
<!-- error.vue -->
<template>
<div>
<h1>{{ error.statusCode }}</h1>
<p>{{ error.message }}</p>
<button @click="handleError">Go Home</button>
</div>
</template>
<script setup lang="ts">
const { error } = defineProps<{
error: { statusCode: number; message: string }
}>()
function handleError() {
clearError({ redirect: "/" })
}
</script>
```
## Environment Variables and Config
### Runtime Config
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Private (server-only)
apiSecret: process.env.API_SECRET,
databaseUrl: process.env.DATABASE_URL,
// Public (exposed to client)
public: {
apiBase: process.env.API_BASE_URL || "http://localhost:3000",
environment: process.env.NODE_ENV,
},
},
})
// Usage
const config = useRuntimeConfig()
console.log(config.public.apiBase) // Available everywhere
console.log(config.apiSecret) // Server-only
```
### App Config
For non-sensitive configuration that can be updated at runtime:
```typescript
// app.config.ts
export default defineAppConfig({
theme: {
primaryColor: "#3b82f6",
},
})
// Usage
const appConfig = useAppConfig()
console.log(appConfig.theme.primaryColor)
```
## Server API Routes
### GET Request
```typescript
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
return {
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
],
}
})
```
### POST Request
```typescript
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate and save user
return { success: true, user: body }
})
```
### Dynamic Routes
```typescript
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id")
// Fetch user by id
return { id, name: "User" }
})
```
### Error Handling in API Routes
```typescript
export default defineEventHandler(async (event) => {
try {
const data = await fetchData()
return data
} catch (error) {
throw createError({
statusCode: 500,
message: "Internal server error",
})
}
})
```
### Protected API Routes
```typescript
// server/api/admin/users.get.ts
export default defineEventHandler(async (event) => {
const session = await requireUserSession(event)
if (!session.user.isAdmin) {
throw createError({
statusCode: 403,
message: "Forbidden",
})
}
return { users: [] }
})
```
## Middleware
### Route Middleware
```typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useState("user")
if (!user.value) {
return navigateTo("/login")
}
})
// Usage in page
definePageMeta({
middleware: "auth",
})
```
### Global Middleware
```typescript
// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Track page view
console.log("Navigating to:", to.path)
})
```
## State Management
### useState
For shared state across components:
```typescript
// composables/useAuth.ts
export const useAuth = () => {
const user = useState<User | null>("user", () => null)
const isAuthenticated = computed(() => !!user.value)
async function login(credentials: LoginCredentials) {
const response = await $fetch("/api/auth/login", {
method: "POST",
body: credentials,
})
user.value = response.user
}
function logout() {
user.value = null
}
return {
user,
isAuthenticated,
login,
logout,
}
}
// Usage in component
const { user, login, logout } = useAuth()
```
## Composables
### Auto-Import from composables/
```typescript
// composables/useCounter.ts
export const useCounter = () => {
const count = ref(0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
increment,
decrement,
}
}
// Usage (auto-imported)
const { count, increment } = useCounter()
```
## Layouts
### Default Layout
```vue
<!-- layouts/default.vue -->
<template>
<div>
<header>
<nav>Navigation</nav>
</header>
<main>
<slot />
</main>
<footer>Footer</footer>
</div>
</template>
```
### Custom Layout
```vue
<!-- layouts/admin.vue -->
<template>
<div class="admin-layout">
<aside>Sidebar</aside>
<main>
<slot />
</main>
</div>
</template>
<!-- Usage in page -->
<script setup lang="ts">
definePageMeta({
layout: "admin",
})
</script>
```
### Dynamic Layout
```typescript
setPageLayout("admin")
```
## Plugins
### Client-Only Plugin
```typescript
// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
// Only runs on client
console.log("Client-side analytics initialized")
})
```
### Server-Only Plugin
```typescript
// plugins/database.server.ts
export default defineNuxtPlugin(() => {
// Only runs on server
return {
provide: {
db: createDatabaseConnection(),
},
}
})
```
### Universal Plugin
```typescript
// plugins/api.ts
export default defineNuxtPlugin(() => {
const api = $fetch.create({
baseURL: "/api",
onResponseError({ response }) {
if (response.status === 401) {
navigateTo("/login")
}
},
})
return {
provide: {
api,
},
}
})
// Usage
const { $api } = useNuxtApp()
const data = await $api("/users")
```
## Auto-Imported APIs
Never manually import these - Nuxt auto-imports them:
**Vue APIs:** `ref`, `reactive`, `computed`, `watch`, `onMounted`, `defineProps`, `defineEmits`, `defineModel`
**Nuxt Composables:** `useState`, `useFetch`, `useAsyncData`, `useRoute`, `useRouter`, `navigateTo`, `useCookie`, `useHead`, `useSeoMeta`, `useRuntimeConfig`, `showError`, `clearError`
## File-Based Conventions
**Routing:**
- `pages/index.vue``/`
- `pages/about.vue``/about`
- `pages/users/[id].vue``/users/:id`
**Server API:**
- `server/api/users.get.ts``/api/users` (GET)
- `server/api/users.post.ts``/api/users` (POST)
- `server/routes/healthz.ts``/healthz`
**Layouts & Middleware:**
- `layouts/default.vue` - Default layout
- `middleware/auth.ts` - Named middleware
- `middleware/analytics.global.ts` - Global middleware
## Nuxt CLI Commands
**Development:**
- `nuxt dev` - Start dev server
- `nuxt dev --host` - Expose to network
**Building:**
- `nuxt build` - Production build
- `nuxt generate` - Static site generation
- `nuxt preview` - Preview production build
**Analysis:**
- `nuxt analyze` - Bundle size analysis
- `nuxt typecheck` - Type checking
- `nuxt info` - Environment info

97
plugin.lock.json Normal file
View File

@@ -0,0 +1,97 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:lttr/claude-marketplace:plugins/nuxt",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "626e443911e81c6bdccf5b6bd0ecadcd626d3733",
"treeHash": "d1094c64598ae325003608e536199af804cad0457de4d4bd64963d1ee8becbaf",
"generatedAt": "2025-11-28T10:20:21.411033Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "nuxt",
"description": "Nuxt development guidance with Vue best practices, auto-imports awareness, and library-specific patterns for Pinia, VueUse, and more",
"version": "2.3.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "ce260d90f9531dd5cb3b65c3adb1f4050ec5be3a3282429698ab86cb84ceb806"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "0621531f83fc2be0ff2a79595811dee13f9a50a30ddbd4039fe566172e3a2c0a"
},
{
"path": "commands/prime/components.md",
"sha256": "b64abab9be3a60502fa7ce9e9c9fa586cd9ceef725c49dd59fd3df3324f1c930"
},
{
"path": "commands/prime/framework.md",
"sha256": "18850a293a59d872751df102f08362ea3bc9d97146a4b8a38495e24301537ac4"
},
{
"path": "skills/nuxt/SKILL.md",
"sha256": "d5c80930f725ce0ea65bdabb4ffe297fee73fd314be87b8aec8fce1fa56b521e"
},
{
"path": "skills/nuxt/references/tailwind.md",
"sha256": "4fa934006a2c52fc99326c4639cf8fdd585e9a7503dc86c350b064421badf044"
},
{
"path": "skills/nuxt/references/nuxt-i18n.md",
"sha256": "f6d8a1c722b11932ff7d533586432dd734f8bc6088009df9770e6f2e170acd0e"
},
{
"path": "skills/nuxt/references/vueuse.md",
"sha256": "e571d8079e0e328cf43323414e1ed96d7118bf157068f2b64a1afa55d235f256"
},
{
"path": "skills/nuxt/references/nuxt-ui.md",
"sha256": "0e9c12178a1c27825d261187dfbf3941b02a94bff5dcee8b099aa653aecb1ebd"
},
{
"path": "skills/nuxt/references/nuxt-patterns.md",
"sha256": "a54b6ae1da4f4c538b5ec618b34dfa035a5fc616f2b633095708872fc618a0c0"
},
{
"path": "skills/nuxt/references/drizzle-db0.md",
"sha256": "ade751a4f2fa0be17f969dbafb8675b727e3b34c2f69b1c1c715bcfa9d81ec75"
},
{
"path": "skills/nuxt/references/nuxt-modules.md",
"sha256": "60bdc911bc49a56f8abdc50f2ed60107a4a597ccc07aeca149624c15c5dbf2d0"
},
{
"path": "skills/nuxt/references/nuxt-image.md",
"sha256": "b5260c00c65c3109813dcf16991b7744f1e216cc26a6213fab05cec00987c404"
},
{
"path": "skills/nuxt/references/vue-best-practices.md",
"sha256": "fb076419a4e5dd9de6c969dedd6968371043a8fa518138ae993aa9dc8a1a02ef"
},
{
"path": "skills/nuxt/references/nuxt-content.md",
"sha256": "2edfeb5744ce32ef392b652914e55dc275fb279e46d71458c85c9392a4228b43"
},
{
"path": "skills/nuxt/references/pinia.md",
"sha256": "4dfd9a2088b1514060dc253becd026003ec745039b1a23687f90a9190ca1cb6e"
}
],
"dirSha256": "d1094c64598ae325003608e536199af804cad0457de4d4bd64963d1ee8becbaf"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

313
skills/nuxt/SKILL.md Normal file
View File

@@ -0,0 +1,313 @@
---
name: nuxt
description: This skill should be used when working on Nuxt projects (v3+). Use it for building Vue applications with Nuxt's file-based routing, server-side rendering, and auto-import features. Trigger when working with .vue or .ts files in Nuxt directories (pages/, server/, composables/, middleware/), nuxt.config.ts, or when the project contains a nuxt dependency in package.json. Also trigger for questions about Nuxt concepts like composables, auto-imports, server routes, SSR/SSG patterns, or file-based routing.
---
# Nuxt Development
## Overview
This skill provides specialized guidance for developing Nuxt applications (v3+), including Vue best practices, Nuxt-specific conventions, ecosystem library knowledge, and access to up-to-date documentation.
## When to Use This Skill
Trigger this skill when:
- Working in a project with `nuxt` as a dependency in package.json
- Creating or editing `.vue` single-file components
- Working with `.ts` or `.tsx` files in Nuxt directories: `pages/`, `server/`, `composables/`, `middleware/`, `layouts/`, or `utils/`
- Working with Nuxt-specific files: `nuxt.config.ts`, `app.vue`, or any file in Nuxt convention directories
- Questions about Nuxt architecture, routing, or SSR/SSG patterns
- User mentions Nuxt-specific concepts: composables, auto-imports, server routes, server API, middleware, file-based routing
- Debugging Nuxt-specific issues or errors
## Documentation Access
### Official Nuxt Documentation
Fetch up-to-date Nuxt documentation when needed:
```
https://nuxt.com/llms.txt
```
Fetch when:
- Uncertain about current Nuxt API syntax or conventions
- User asks about specific Nuxt features or modules
- Working with recently released Nuxt features
- Encountering Nuxt-specific errors or configuration issues
- Need to verify patterns work with the specific Nuxt version in use
## Quick Reference
### Auto-Imported APIs (No Import Needed)
Nuxt automatically imports these without explicit import statements:
**Vue APIs:** `ref`, `reactive`, `computed`, `watch`, `onMounted`, `defineProps`, `defineEmits`, `defineModel`
**Nuxt Composables:** `useState`, `useFetch`, `useAsyncData`, `useRoute`, `useRouter`, `navigateTo`, `useCookie`, `useHead`, `useSeoMeta`, `useRuntimeConfig`, `showError`, `clearError`
**Auto-imports:**
- Components from `components/` directory
- Composables from `composables/` directory
- Server utilities from `server/utils/` directory
### File-Based Conventions
**Routing:**
- `pages/index.vue``/`
- `pages/about.vue``/about`
- `pages/users/[id].vue``/users/:id` (dynamic route)
**Server API:**
- `server/api/users.get.ts``/api/users` (GET endpoint)
- `server/api/users.post.ts``/api/users` (POST endpoint)
- `server/routes/healthz.ts``/healthz` (custom route)
**Layouts & Middleware:**
- `layouts/default.vue` - Default layout
- `middleware/auth.ts` - Named middleware (use via `definePageMeta({ middleware: 'auth' })`)
- `middleware/analytics.global.ts` - Global middleware (runs on every route)
### Nuxt CLI Commands
**Development:**
- `nuxt dev` - Start development server
- `nuxt dev --host` - Expose to network
**Building:**
- `nuxt build` - Production build
- `nuxt generate` - Static site generation
- `nuxt preview` - Preview production build
**Analysis:**
- `nuxt analyze` - Bundle size analysis
- `nuxt typecheck` - Type checking
- `nuxt info` - Environment info for bug reports
## Project Dependency Detection
**Important:** Before providing library-specific guidance, check if the library is installed by examining `package.json`. Only include library-specific advice for dependencies that exist in the project.
### Core Libraries (Always Available)
- **Vue** - Component framework (auto-imported)
- **Vue Router** - Routing (file-based, managed by Nuxt)
- **Nitro** - Server engine (built into Nuxt)
### Optional Libraries to Check
Check `package.json` for these before suggesting their features:
**State & Utilities:**
- `pinia` or `@pinia/nuxt` - State management → See `references/pinia.md`
- `@vueuse/core` or `@vueuse/nuxt` - Composition utilities → See `references/vueuse.md`
- `drizzle-orm` - Database ORM → See `references/drizzle-db0.md`
**Core Nuxt Modules (Dedicated References):**
- `@nuxt/ui` - UI component library → See `references/nuxt-ui.md`
- `@nuxt/image` - Image optimization → See `references/nuxt-image.md`
- `@nuxt/content` - File-based CMS → See `references/nuxt-content.md`
- `@nuxtjs/i18n` - Internationalization → See `references/nuxt-i18n.md`
- `@nuxtjs/tailwindcss` - Tailwind CSS → See `references/tailwind.md`
**Other Nuxt Modules:**
- `@nuxt/icon`, `@nuxtjs/seo`, `@nuxtjs/color-mode` → See `references/nuxt-modules.md`
- `@nuxt/eslint`, `@nuxt/fonts`, `@nuxt/scripts`, `nuxt-security` → See `references/nuxt-modules.md`
## References
This skill includes detailed reference documentation for specific topics. Load these files as needed when working with specific features:
### Core Best Practices
**`references/vue-best-practices.md`** - Vue component patterns and conventions
- Use when writing or reviewing Vue components
- Covers: Script setup syntax, props/emits/v-model, component structure, template directives, reactivity patterns
**`references/nuxt-patterns.md`** - Common Nuxt patterns and recipes
- Use when implementing features or solving common tasks
- Covers: Data fetching, SEO/meta tags, error handling, environment config, server API routes, middleware, state management, composables, layouts, plugins
### Core Nuxt Modules (Comprehensive Documentation)
**`references/nuxt-ui.md`** - Nuxt UI component library (Last updated: 2025-01)
- Only use if `@nuxt/ui` is installed
- Covers: v3/v4 setup and migration, components (forms, buttons, modals, tables), Tailwind v4 integration, theming, validation, troubleshooting
- **Important:** Includes version-specific breaking changes and setup requirements
**`references/tailwind.md`** - Tailwind CSS in Nuxt (Last updated: 2025-01)
- Only use if `@nuxtjs/tailwindcss` is installed
- Covers: v3/v4 setup, configuration, responsive design, dark mode, custom utilities, plugins, JIT mode, performance optimization
**`references/nuxt-image.md`** - Image optimization (Last updated: 2025-01)
- Only use if `@nuxt/image` is installed
- Covers: NuxtImg/NuxtPicture components, image providers, lazy loading, responsive images, performance optimization
**`references/nuxt-content.md`** - File-based CMS (Last updated: 2025-01)
- Only use if `@nuxt/content` is installed
- Covers: Markdown/YAML content, queryContent API, components (ContentDoc, ContentList), navigation, search, pagination, syntax highlighting
**`references/nuxt-i18n.md`** - Internationalization (Last updated: 2025-01)
- Only use if `@nuxtjs/i18n` is installed
- Covers: Multi-language routing, translations, locale switching, SEO, number/date formatting, pluralization, composables
### State Management & Utilities
**`references/pinia.md`** - Pinia state management
- Only use if `pinia` or `@pinia/nuxt` is installed
- Covers: Store definition, component usage, SSR, persistence, testing
**`references/vueuse.md`** - VueUse composables
- Only use if `@vueuse/core` or `@vueuse/nuxt` is installed
- Covers: State management composables, browser APIs, element interaction, utilities, common patterns
**`references/drizzle-db0.md`** - Database with Drizzle ORM
- Only use if `drizzle-orm` is installed
- Covers: Setup, schema definition, CRUD operations, queries, joins, filtering, transactions, migrations, type safety
### Other Modules
**`references/nuxt-modules.md`** - Other official Nuxt modules
- Brief overview of: @nuxt/icon, @nuxtjs/seo, @nuxtjs/color-mode, @nuxt/eslint, @nuxt/fonts, @nuxt/scripts, nuxt-security
- For detailed guidance on @nuxt/ui, @nuxt/image, @nuxt/content, @nuxtjs/i18n, or @nuxtjs/tailwindcss, use their dedicated reference files instead
## How to Use This Skill
1. **Check dependencies** - Always examine `package.json` first to know what libraries are available
2. **Follow Vue best practices** - Apply patterns from `vue-best-practices.md` to all component code
3. **Leverage auto-imports** - Never manually import Nuxt/Vue composables that are auto-imported
4. **Use file-based conventions** - Follow Nuxt's directory structure for routing, APIs, and middleware
5. **Reference library docs** - When a library is installed, consult its reference file for specific patterns
6. **Verify version-specific features** - Reference files include "Last updated" dates; always verify with official docs for version-specific details
7. **Fetch official docs** - For recent features or uncertainty, fetch from https://nuxt.com/llms.txt or module-specific documentation URLs
### Version-Specific Information
- **Reference files with dates** (marked "Last updated: YYYY-MM") contain version-specific info
- **Always verify with official docs** when:
- Working with modules not documented in references
- Module version differs significantly from documentation date
- Encountering breaking changes or migration scenarios
- Uncertain about syntax or API for current version
- **Fallback principle:** If reference documentation doesn't match project needs, fetch official docs rather than guessing
## Important Conventions
### Component Files Must:
- Use `<script setup lang="ts">` syntax
- Place `<template>` section first, before `<script>` and `<style>`
- Use type-based `defineProps()`, `defineEmits()`, and `defineModel()`
- Use multi-word component names (except pages/layouts)
- Use `v-for="item of items"` with `:key`
- Prefer `ref()` over `reactive()`
### TypeScript Organization:
- Place all types/interfaces in `/types` directory (or `/app/types` in Nuxt 4)
- Organize by domain: `types/user.ts`, `types/post.ts`, `types/auth.ts`
- **NO barrel exports** - import directly from specific files: `import type { User } from '~/types/user'`
- Use PascalCase naming conventions:
- Props interfaces: `ButtonProps`, `CardProps`
- State interfaces: `AuthState`, `UserState`
- API types: `CreateUserRequest`, `CreateUserResponse`
- **Never use `as any`** - prefer type guards, type narrowing, or `as unknown as Type` when absolutely necessary
### File Structure (Nuxt 4):
- Nuxt 4 supports optional `/app` directory for app-specific code
- Components can live in `/components` or `/app/components`
- Composables can live in `/composables` or `/app/composables`
- Types can live in `/types` or `/app/types`
- Both root-level and `/app` directory structures are supported
### Data Fetching State Handling:
- Always use `status` property (not deprecated `pending`)
- Status values: `'idle' | 'pending' | 'success' | 'error'`
- Always destructure: `const { data, status, error } = await useFetch(...)`
- Handle all states in templates:
```vue
<div v-if="status === 'pending'">Loading...</div>
<div v-else-if="status === 'error'">Error: {{ error }}</div>
<div v-else>{{ data }}</div>
```
### Styling Strategy:
- Check `package.json` for `@nuxtjs/tailwindcss` dependency
- **If Tailwind is installed:** Prefer Tailwind utility classes in templates
- Use arbitrary variants for scrollbars: `[&::-webkit-scrollbar]:w-1.5`
- Use `@theme` directive for custom animations and CSS variables
- Use arbitrary variants for pseudo-elements: `before:content-['★']`
- **If Tailwind is NOT installed:** Use `<style scoped>` for component styles
- **Use `<style>` only for:** Very complex keyframes, cross-browser scrollbars, or unreadable utility expressions
### Accessibility:
- Add appropriate ARIA attributes to interactive elements
- Ensure keyboard navigation support (tab order, enter/space handlers)
- Use semantic HTML elements (`<button>`, `<nav>`, `<main>`, etc.)
### Nuxt Projects Should:
- Never manually import auto-imported composables
- Use `useFetch` for API calls instead of manual fetch
- Define server routes in `server/api/` with `.get.ts`, `.post.ts` naming
- Use `useState` for shared state across components
- Use `definePageMeta` for page-specific config (middleware, layout, etc.)
### When Libraries Are Installed:
- **Pinia** - Use for complex state management across many components
- **VueUse** - Prefer VueUse composables over custom implementations for common patterns
- **Drizzle** - Use for type-safe database operations with full TypeScript inference
### VueUse Integration Guidelines:
When encountering custom utility implementations for common patterns, check if VueUse provides an equivalent solution:
- **State patterns:** `useAsyncData`, `useToggle`, `useCounter`, `useLocalStorage`, `useSessionStorage`
- **DOM interactions:** `useMouse`, `useScroll`, `useElementVisibility`, `useIntersectionObserver`, `useResizeObserver`
- **Browser APIs:** `useClipboard`, `useMediaQuery`, `useDark`, `usePreferredDark`, `useGeolocation`
- **Utilities:** `refDebounced`, `useDebounceFn`, `refThrottled`, `useThrottleFn`, `useInterval`, `useTimeout`
**When to suggest VueUse:**
- Detecting bespoke implementations of the above patterns
- User asks about utilities for common tasks (debouncing, throttling, etc.)
- Building features that require browser API abstractions
**Only suggest if:**
- `@vueuse/core` or `@vueuse/nuxt` is in package.json, OR
- User explicitly asks about VueUse or requests suggestions for utility libraries
**Never:**
- Force VueUse if not installed
- Suggest VueUse for simple one-off logic that doesn't need a composable

View File

@@ -0,0 +1,313 @@
# Database with Drizzle ORM and db0
**Check if installed:** Look for `drizzle-orm` and optionally `db0` in package.json before using.
## Overview
Drizzle ORM is a TypeScript-first ORM. In Nuxt, it's commonly used with Nitro's `db0` for database access.
## Setup
### Database Configuration
```typescript
// server/database/index.ts
import { drizzle } from "drizzle-orm/better-sqlite3"
import Database from "better-sqlite3"
const sqlite = new Database("sqlite.db")
export const db = drizzle(sqlite)
```
### Schema Definition
```typescript
// server/database/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
() => new Date(),
),
})
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
content: text("content"),
authorId: integer("author_id").references(() => users.id),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
() => new Date(),
),
})
```
## CRUD Operations
### Insert
```typescript
// server/api/users.post.ts
import { db } from "~/server/database"
import { users } from "~/server/database/schema"
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const [user] = await db
.insert(users)
.values({
name: body.name,
email: body.email,
})
.returning()
return user
})
```
### Select
```typescript
// server/api/users.get.ts
import { db } from "~/server/database"
import { users } from "~/server/database/schema"
import { eq } from "drizzle-orm"
export default defineEventHandler(async () => {
// Get all users
const allUsers = await db.select().from(users)
// Get user by id
const user = await db.select().from(users).where(eq(users.id, 1))
// Get with specific columns
const userEmails = await db
.select({
email: users.email,
name: users.name,
})
.from(users)
return allUsers
})
```
### Update
```typescript
import { db } from "~/server/database"
import { users } from "~/server/database/schema"
import { eq } from "drizzle-orm"
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id")
const body = await readBody(event)
const [updated] = await db
.update(users)
.set({ name: body.name })
.where(eq(users.id, Number(id)))
.returning()
return updated
})
```
### Delete
```typescript
import { db } from "~/server/database"
import { users } from "~/server/database/schema"
import { eq } from "drizzle-orm"
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id")
await db.delete(users).where(eq(users.id, Number(id)))
return { success: true }
})
```
## Queries
### With Joins
```typescript
import { db } from "~/server/database"
import { users, posts } from "~/server/database/schema"
import { eq } from "drizzle-orm"
export default defineEventHandler(async () => {
const usersWithPosts = await db
.select()
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId))
return usersWithPosts
})
```
### Relational Queries
```typescript
// Define relations
import { relations } from "drizzle-orm"
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}))
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}))
// Query with relations
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
})
```
### Filtering
```typescript
import { eq, ne, gt, gte, lt, lte, like, and, or } from "drizzle-orm"
// Equal
const user = await db.select().from(users).where(eq(users.id, 1))
// Greater than
const recentPosts = await db
.select()
.from(posts)
.where(gt(posts.createdAt, new Date("2024-01-01")))
// Like (pattern matching)
const searchUsers = await db
.select()
.from(users)
.where(like(users.name, "%john%"))
// Multiple conditions
const filtered = await db
.select()
.from(users)
.where(
and(eq(users.active, true), gt(users.createdAt, new Date("2024-01-01"))),
)
```
### Ordering and Limiting
```typescript
import { desc, asc } from "drizzle-orm"
const latestPosts = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(10)
const paginatedUsers = await db
.select()
.from(users)
.orderBy(asc(users.name))
.limit(20)
.offset(40)
```
## Transactions
```typescript
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const result = await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({
name: body.name,
email: body.email,
})
.returning()
const [post] = await tx
.insert(posts)
.values({
title: body.postTitle,
authorId: user.id,
})
.returning()
return { user, post }
})
return result
})
```
## Migrations
### Generate Migration
```bash
npx drizzle-kit generate:sqlite
```
### Run Migration
```typescript
// server/database/migrate.ts
import { migrate } from "drizzle-orm/better-sqlite3/migrator"
import { db } from "./index"
migrate(db, { migrationsFolder: "./drizzle" })
```
## Type Safety
Drizzle provides full TypeScript type inference:
```typescript
// Types are inferred automatically
const users = await db.select().from(users)
// users: { id: number; name: string; email: string; createdAt: Date }[]
// Partial selects are typed too
const names = await db.select({ name: users.name }).from(users)
// names: { name: string }[]
```
## Using with db0 (Nitro)
```typescript
// server/database/index.ts
import { drizzle } from "drizzle-orm/d1"
export default defineNitroPlugin(() => {
// Access db0 database
const db = drizzle(useDatabase())
return {
provide: {
db,
},
}
})
// Usage in API routes
export default defineEventHandler(async (event) => {
const { $db } = event.context
const users = await $db.select().from(users)
return users
})
```

View File

@@ -0,0 +1,827 @@
# Nuxt Content Reference
**Last Updated:** 2025-11 (v3.8.0)
**Check:** `@nuxt/content` in package.json
Nuxt Content v3 is a Git-based CMS that uses SQL-backed collections for querying markdown, YAML, JSON, and CSV files. Perfect for blogs, documentation sites, and content-heavy applications.
## Installation & Setup
```bash
pnpm add @nuxt/content
```
**nuxt.config.ts:**
```typescript
export default defineNuxtConfig({
modules: ["@nuxt/content"],
content: {
// Optional configuration
highlight: {
theme: "github-dark",
preload: ["typescript", "vue", "bash"],
},
},
})
```
## Collections
### Defining Collections
Create `content.config.ts` in your project root:
```typescript
import { defineCollection, defineContentConfig, z } from "@nuxt/content"
export default defineContentConfig({
collections: {
blog: defineCollection({
type: "page",
source: "blog/**/*.md",
schema: z.object({
title: z.string(),
description: z.string(),
date: z.string(),
tags: z.array(z.string()).optional(),
author: z.string().optional(),
image: z.string().optional(),
draft: z.boolean().default(false),
}),
}),
docs: defineCollection({
type: "page",
source: "docs/**/*.md",
schema: z.object({
title: z.string(),
description: z.string(),
category: z.string().optional(),
}),
}),
},
})
```
### Directory Structure
```
content/
├── blog/
│ ├── post-1.md
│ ├── post-2.md
│ └── post-3.md
└── docs/
├── getting-started.md
└── api/
└── components.md
```
## Frontmatter
Add metadata to content files:
```markdown
---
title: "My Blog Post"
description: "A brief description"
date: "2025-01-04"
tags: ["nuxt", "vue", "web-dev"]
author: "John Doe"
image: "/images/cover.jpg"
draft: false
---
# Content starts here
Your markdown content...
```
## Querying Collections
### queryCollection Composable
The main API for querying content in v3:
```vue
<script setup lang="ts">
// Get all articles from 'blog' collection
const { data: articles } = await useAsyncData("articles", () =>
queryCollection("blog").all(),
)
// Get single article by path
const route = useRoute()
const { data: article } = await useAsyncData("article", () =>
queryCollection("blog").path(route.path).first(),
)
</script>
<template>
<div>
<h1>{{ article?.title }}</h1>
<ContentRenderer v-if="article" :value="article" />
</div>
</template>
```
### Query Methods
#### all()
Get all matching documents:
```typescript
const posts = await queryCollection("blog").all()
```
#### first()
Get first matching document:
```typescript
const post = await queryCollection("blog").path("/blog/my-post").first()
```
#### where()
Filter by field with SQL operators:
```typescript
// Single condition
const published = await queryCollection("blog").where("draft", "=", false).all()
// Multiple conditions (AND)
const filtered = await queryCollection("blog")
.where("draft", "=", false)
.where("category", "=", "tech")
.all()
```
#### andWhere()
Complex AND conditions:
```typescript
const posts = await queryCollection("blog")
.where("published", "=", true)
.andWhere((query) =>
query.where("date", ">", "2024-01-01").where("category", "=", "news"),
)
.all()
```
#### orWhere()
OR conditions:
```typescript
const posts = await queryCollection("blog")
.where("published", "=", true)
.orWhere((query) =>
query.where("featured", "=", true).where("priority", ">", 5),
)
.all()
```
#### order()
Sort results:
```typescript
// Descending
const posts = await queryCollection("blog").order("date", "DESC").all()
// Ascending
const posts = await queryCollection("blog").order("title", "ASC").all()
```
#### limit()
Limit results:
```typescript
const latest = await queryCollection("blog")
.order("date", "DESC")
.limit(5)
.all()
```
#### skip()
Skip results (for pagination):
```typescript
const page = 2
const perPage = 10
const posts = await queryCollection("blog")
.order("date", "DESC")
.skip((page - 1) * perPage)
.limit(perPage)
.all()
```
#### select()
Select specific fields:
```typescript
const posts = await queryCollection("blog")
.select(["title", "description", "date", "path"])
.all()
```
#### path()
Filter by path:
```typescript
const post = await queryCollection("blog").path("/blog/my-post").first()
```
## ContentRenderer Component
Renders parsed markdown content (the main component for v3):
```vue
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData("post", () =>
queryCollection("blog").path(route.path).first(),
)
</script>
<template>
<article v-if="post" class="prose dark:prose-invert">
<h1>{{ post.title }}</h1>
<ContentRenderer :value="post" />
</article>
</template>
```
With error handling:
```vue
<script setup lang="ts">
const route = useRoute()
const { data: post, error } = await useAsyncData("post", () =>
queryCollection("blog").path(route.path).first(),
)
if (error.value || !post.value) {
throw createError({
statusCode: 404,
message: "Post not found",
})
}
</script>
<template>
<ContentRenderer :value="post" />
</template>
```
## Common Patterns
### Blog List Page
```vue
<script setup lang="ts">
const { data: articles } = await useAsyncData("articles", () =>
queryCollection("blog")
.where("draft", "=", false)
.order("date", "DESC")
.all(),
)
</script>
<template>
<div>
<h1>Blog</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<article v-for="article of articles" :key="article.path" class="card">
<NuxtImg
v-if="article.image"
:src="article.image"
:alt="article.title"
class="w-full h-48 object-cover"
/>
<div class="p-4">
<h2 class="text-xl font-bold">{{ article.title }}</h2>
<p class="text-gray-600">{{ article.description }}</p>
<div class="flex items-center gap-2 mt-2 text-sm text-gray-500">
<span>{{ article.author }}</span>
<span></span>
<time>{{ article.date }}</time>
</div>
<NuxtLink
:to="article.path"
class="text-blue-500 hover:underline mt-2 inline-block"
>
Read more
</NuxtLink>
</div>
</article>
</div>
</div>
</template>
```
### Single Blog Post with Related Articles
```vue
<script setup lang="ts">
const route = useRoute()
const { data: article } = await useAsyncData("article", () =>
queryCollection("blog").path(route.path).first(),
)
if (!article.value) {
throw createError({
statusCode: 404,
message: "Article not found",
})
}
// Get related articles by tags
const { data: related } = await useAsyncData("related", async () => {
if (!article.value?.tags?.length) return []
const all = await queryCollection("blog").where("draft", "=", false).all()
// Filter by matching tags
return all
.filter(
(post) =>
post.path !== route.path &&
post.tags?.some((tag) => article.value.tags.includes(tag)),
)
.slice(0, 3)
})
useSeoMeta({
title: article.value.title,
description: article.value.description,
ogImage: article.value.image,
})
</script>
<template>
<article class="prose dark:prose-invert mx-auto">
<h1>{{ article.title }}</h1>
<div class="flex items-center gap-4 text-gray-600 mb-8">
<span>{{ article.author }}</span>
<time>{{ article.date }}</time>
<div v-if="article.tags" class="flex gap-2">
<span v-for="tag of article.tags" :key="tag" class="badge">
{{ tag }}
</span>
</div>
</div>
<ContentRenderer :value="article" />
<div v-if="related?.length" class="mt-12">
<h2>Related Articles</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div v-for="post of related" :key="post.path">
<NuxtLink :to="post.path">{{ post.title }}</NuxtLink>
</div>
</div>
</div>
</article>
</template>
```
### Documentation Site
```vue
<!-- pages/docs/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
const slug = (route.params.slug as string[])?.join("/") || "index"
const { data: doc } = await useAsyncData(`doc-${slug}`, () =>
queryCollection("docs").path(`/docs/${slug}`).first(),
)
if (!doc.value) {
throw createError({ statusCode: 404, message: "Documentation not found" })
}
// Get navigation
const { data: navigation } = await useAsyncData("docs-nav", () =>
queryCollectionNavigation("docs"),
)
useSeoMeta({
title: doc.value.title,
description: doc.value.description,
})
</script>
<template>
<div class="flex">
<!-- Sidebar navigation -->
<aside class="w-64 pr-8">
<nav>
<ul>
<li v-for="link of navigation" :key="link.path">
<NuxtLink :to="link.path">{{ link.title }}</NuxtLink>
</li>
</ul>
</nav>
</aside>
<!-- Main content -->
<main class="flex-1">
<article class="prose dark:prose-invert">
<h1>{{ doc.title }}</h1>
<ContentRenderer :value="doc" />
</article>
</main>
</div>
</template>
```
### Search
```vue
<script setup lang="ts">
const searchQuery = ref("")
const { data: allPosts } = await useAsyncData("all-posts", () =>
queryCollection("blog").all(),
)
const results = computed(() => {
if (!searchQuery.value) return []
const query = searchQuery.value.toLowerCase()
return (
allPosts.value
?.filter(
(post) =>
post.title?.toLowerCase().includes(query) ||
post.description?.toLowerCase().includes(query),
)
.slice(0, 10) || []
)
})
</script>
<template>
<div>
<input
v-model="searchQuery"
type="search"
placeholder="Search articles..."
class="w-full px-4 py-2 border rounded"
/>
<div v-if="results.length" class="mt-4">
<div v-for="result of results" :key="result.path" class="mb-4">
<NuxtLink :to="result.path" class="text-lg font-bold">
{{ result.title }}
</NuxtLink>
<p class="text-gray-600">{{ result.description }}</p>
</div>
</div>
</div>
</template>
```
### Pagination
```vue
<script setup lang="ts">
const route = useRoute()
const page = computed(() => parseInt(route.query.page as string) || 1)
const perPage = 10
const { data: posts } = await useAsyncData(
`posts-page-${page.value}`,
() =>
queryCollection("blog")
.where("draft", "=", false)
.order("date", "DESC")
.skip((page.value - 1) * perPage)
.limit(perPage)
.all(),
{
watch: [page],
},
)
// Get total count for pagination
const { data: allPosts } = await useAsyncData("all-posts", () =>
queryCollection("blog").where("draft", "=", false).all(),
)
const totalPages = computed(() =>
Math.ceil((allPosts.value?.length || 0) / perPage),
)
</script>
<template>
<div>
<div class="grid gap-6">
<article v-for="post of posts" :key="post.path">
<!-- Article card -->
</article>
</div>
<div class="flex justify-center gap-2 mt-8">
<NuxtLink v-if="page > 1" :to="{ query: { page: page - 1 } }" class="btn">
Previous
</NuxtLink>
<span>Page {{ page }} of {{ totalPages }}</span>
<NuxtLink
v-if="page < totalPages"
:to="{ query: { page: page + 1 } }"
class="btn"
>
Next
</NuxtLink>
</div>
</div>
</template>
```
### Filter by Tags/Categories
```vue
<script setup lang="ts">
const selectedTag = ref<string | null>(null)
// Get all posts to extract unique tags
const { data: allPosts } = await useAsyncData("all-posts", () =>
queryCollection("blog").where("draft", "=", false).all(),
)
const tags = computed(() => {
const tagSet = new Set<string>()
allPosts.value?.forEach((post) => {
post.tags?.forEach((tag: string) => tagSet.add(tag))
})
return Array.from(tagSet).sort()
})
// Filter posts by selected tag
const filteredPosts = computed(() => {
if (!selectedTag.value) return allPosts.value
return allPosts.value?.filter((post) =>
post.tags?.includes(selectedTag.value!),
)
})
</script>
<template>
<div>
<div class="flex gap-2 mb-6">
<button :class="{ active: !selectedTag }" @click="selectedTag = null">
All
</button>
<button
v-for="tag of tags"
:key="tag"
:class="{ active: selectedTag === tag }"
@click="selectedTag = tag"
>
{{ tag }}
</button>
</div>
<div class="grid gap-6">
<article v-for="post of filteredPosts" :key="post.path">
<h2>{{ post.title }}</h2>
<p>{{ post.description }}</p>
<NuxtLink :to="post.path">Read more</NuxtLink>
</article>
</div>
</div>
</template>
```
## Server-Side Queries
Use `queryCollection` in API routes with event parameter:
```typescript
// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
const posts = await queryCollection(event, "blog")
.where("draft", "=", false)
.order("date", "DESC")
.limit(10)
.all()
return posts
})
```
With query parameters:
```typescript
// server/api/posts/[slug].get.ts
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, "slug")
const post = await queryCollection(event, "blog")
.path(`/blog/${slug}`)
.first()
if (!post) {
throw createError({
statusCode: 404,
message: "Post not found",
})
}
return post
})
```
## Navigation
Use `queryCollectionNavigation` for generating navigation:
```vue
<script setup lang="ts">
const { data: navigation } = await useAsyncData("navigation", () =>
queryCollectionNavigation("docs"),
)
</script>
<template>
<nav>
<ul>
<li v-for="item of navigation" :key="item.path">
<NuxtLink :to="item.path">{{ item.title }}</NuxtLink>
<ul v-if="item.children">
<li v-for="child of item.children" :key="child.path">
<NuxtLink :to="child.path">{{ child.title }}</NuxtLink>
</li>
</ul>
</li>
</ul>
</nav>
</template>
```
## Markdown Features
### Syntax Highlighting
Configure in `nuxt.config.ts`:
```typescript
export default defineNuxtConfig({
content: {
highlight: {
theme: {
default: "github-light",
dark: "github-dark",
},
preload: ["typescript", "vue", "bash", "json"],
},
},
})
```
Use in markdown:
````markdown
```typescript
interface User {
name: string
age: number
}
```
````
### Custom Vue Components in Markdown
Use Vue components directly in markdown (MDC syntax):
```markdown
# My Article
::Alert{type="info"}
This is an informational alert!
::
::CallToAction{title="Get Started" url="/docs"}
Learn more about Nuxt Content
::
```
Register components in `components/content/` directory:
```vue
<!-- components/content/Alert.vue -->
<script setup lang="ts">
defineProps<{
type?: "info" | "warning" | "error"
}>()
</script>
<template>
<div :class="`alert alert-${type}`">
<slot />
</div>
</template>
```
## TypeScript Support
Type your collections:
```typescript
// types/content.ts
export interface BlogPost {
title: string
description: string
date: string
tags?: string[]
author?: string
image?: string
draft: boolean
path: string
body: any
}
```
Use in components:
```vue
<script setup lang="ts">
import type { BlogPost } from "~/types/content"
const { data: posts } = await useAsyncData("posts", () =>
queryCollection<BlogPost>("blog").all(),
)
</script>
```
## Migration from v2
### Key Changes
1. **`queryContent()` → `queryCollection(name)`**
```typescript
// v2
queryContent("/blog").find()
// v3
queryCollection("blog").all()
```
2. **Collections must be defined in `content.config.ts`**
3. **Components removed:**
- ❌ `<ContentDoc>` - use ContentRenderer
- ❌ `<ContentList>` - query manually with queryCollection
- ❌ `<ContentNavigation>` - use queryCollectionNavigation
- ❌ `<ContentQuery>` - use queryCollection
4. **New query methods:**
- `.all()` instead of `.find()`
- `.first()` instead of `.findOne()`
- `.where()` with SQL operators
- `.order()` instead of `.sort()`
5. **SQL-backed storage** - faster queries for large datasets
## Best Practices
1. **Define collections** - Always create `content.config.ts` with schemas
2. **Use TypeScript** - Type your collections for better DX
3. **Cache queries** - Use `useAsyncData` with proper keys
4. **Server-side queries** - Query on server for API routes
5. **Index for performance** - Consider indexing frequently queried fields
6. **Validate frontmatter** - Use Zod schemas in collection definitions
7. **Handle 404s** - Always check if content exists and throw errors
8. **Use path()** - More efficient than where() for path filtering
9. **Select fields** - Use `.select()` to reduce payload size
10. **Pagination** - Implement for large collections
## Official Resources
- **Documentation:** https://content.nuxt.com
- **Collections:** https://content.nuxt.com/docs/collections
- **Query API:** https://content.nuxt.com/docs/utils/query-collection
- **Migration Guide:** https://content.nuxt.com/docs/getting-started/migration
- **GitHub:** https://github.com/nuxt/content
---
**Note:** This reference covers Nuxt Content v3. The v2 API (`queryContent`, `ContentDoc`, `ContentList`) is deprecated and not compatible with v3.

View File

@@ -0,0 +1,757 @@
# Nuxt I18n Reference
**Last Updated:** 2025-11 (v10.2.0)
**Check:** `@nuxtjs/i18n` in package.json
Nuxt I18n v10 provides internationalization (i18n) for Nuxt applications with auto-imports, locale routing, SEO support, and integration with Vue I18n v11.
## Installation & Setup
```bash
pnpm add @nuxtjs/i18n
```
**nuxt.config.ts:**
```typescript
export default defineNuxtConfig({
modules: ["@nuxtjs/i18n"],
i18n: {
locales: [
{ code: "en", iso: "en-US", name: "English", file: "en.json" },
{ code: "fr", iso: "fr-FR", name: "Français", file: "fr.json" },
{ code: "es", iso: "es-ES", name: "Español", file: "es.json" },
],
defaultLocale: "en",
langDir: "locales/",
strategy: "prefix_except_default", // or 'prefix', 'no_prefix'
detectBrowserLanguage: {
useCookie: true,
cookieKey: "i18n_redirected",
redirectOn: "root",
},
},
})
```
## Directory Structure
```
locales/
├── en.json
├── fr.json
└── es.json
```
**Example locale file (en.json):**
```json
{
"welcome": "Welcome",
"hello": "Hello {name}",
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"products": {
"title": "Our Products",
"description": "Browse our collection"
}
}
```
## Basic Usage
### Translation in Templates
```vue
<template>
<div>
<!-- Simple translation -->
<h1>{{ $t("welcome") }}</h1>
<!-- With parameters -->
<p>{{ $t("hello", { name: "John" }) }}</p>
<!-- Nested keys -->
<nav>
<NuxtLink to="/">{{ $t("nav.home") }}</NuxtLink>
<NuxtLink to="/about">{{ $t("nav.about") }}</NuxtLink>
</nav>
<!-- Pluralization -->
<p>{{ $t("items", { count: 5 }) }}</p>
<!-- Number formatting -->
<p>{{ $n(1000, "currency") }}</p>
<!-- Date formatting -->
<p>{{ $d(new Date(), "long") }}</p>
</div>
</template>
```
### Translation in Script
```vue
<script setup lang="ts">
const { t, locale, locales, setLocale } = useI18n()
// Get translation
const welcomeMessage = t("welcome")
// With parameters
const greeting = t("hello", { name: "John" })
// Current locale
console.log(locale.value) // 'en'
// Available locales
console.log(locales.value) // [{ code: 'en', ... }, ...]
// Change locale
async function switchLanguage(code: string) {
await setLocale(code)
}
</script>
```
## Routing
### Route Strategies
**prefix_except_default** (recommended):
- Default locale: `/about`
- Other locales: `/fr/about`, `/es/about`
**prefix**:
- All locales: `/en/about`, `/fr/about`, `/es/about`
**no_prefix**:
- All locales use same path: `/about`
- Locale detected from cookie/browser
### Locale Links
```vue
<script setup lang="ts">
const localePath = useLocalePath()
const switchLocalePath = useSwitchLocalePath()
</script>
<template>
<div>
<!-- Link with current locale -->
<NuxtLink :to="localePath('/')">
{{ $t("nav.home") }}
</NuxtLink>
<NuxtLink :to="localePath('/about')">
{{ $t("nav.about") }}
</NuxtLink>
<!-- Switch locale links -->
<NuxtLink :to="switchLocalePath('fr')"> Français </NuxtLink>
<NuxtLink :to="switchLocalePath('es')"> Español </NuxtLink>
<!-- With named routes -->
<NuxtLink :to="localePath({ name: 'products-id', params: { id: '123' } })">
Product
</NuxtLink>
</div>
</template>
```
### Programmatic Navigation
```vue
<script setup lang="ts">
const localePath = useLocalePath()
const router = useRouter()
async function goToAbout() {
await router.push(localePath("/about"))
}
async function goToProduct(id: string) {
await navigateTo(localePath({ name: "products-id", params: { id } }))
}
</script>
```
## Composables
### useI18n
Main composable for translations:
```vue
<script setup lang="ts">
const {
t, // Translation function
locale, // Current locale ref
locales, // Available locales
setLocale, // Change locale function
n, // Number formatting
d, // Date formatting
tm, // Translation messages
te, // Translation exists check
getLocaleCookie, // Get locale cookie
} = useI18n()
// Check if translation exists
if (te("optional.message")) {
console.log(t("optional.message"))
}
// Get all translations for a key
const navTranslations = tm("nav")
console.log(navTranslations) // { home: 'Home', about: 'About', ... }
</script>
```
### useLocalePath
Generate localized paths:
```vue
<script setup lang="ts">
const localePath = useLocalePath()
// Simple path
const homePath = localePath("/")
// With route object
const productPath = localePath({
name: "products-id",
params: { id: "123" },
query: { tab: "reviews" },
})
// With specific locale
const frenchPath = localePath("/about", "fr")
</script>
```
### useSwitchLocalePath
Generate paths for locale switching:
```vue
<script setup lang="ts">
const switchLocalePath = useSwitchLocalePath()
const route = useRoute()
// Get path for switching to French on current page
const frenchPath = switchLocalePath("fr")
// Custom route
const customPath = switchLocalePath("es", "/about")
</script>
```
### useRouteBaseName
Get route name without locale prefix:
```vue
<script setup lang="ts">
const getRouteBaseName = useRouteBaseName()
const route = useRoute()
// Current route: /fr/products/123
const baseName = getRouteBaseName(route) // 'products-id'
</script>
```
### useBrowserLocale
Detect browser locale:
```vue
<script setup lang="ts">
const browserLocale = useBrowserLocale()
console.log(browserLocale) // 'en-US'
</script>
```
## Common Patterns
### Language Switcher
```vue
<script setup lang="ts">
const { locale, locales, setLocale } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const availableLocales = computed(() =>
locales.value.filter((l) => l.code !== locale.value),
)
</script>
<template>
<div class="flex gap-2">
<NuxtLink
v-for="loc of availableLocales"
:key="loc.code"
:to="switchLocalePath(loc.code)"
class="px-3 py-1 rounded hover:bg-gray-100"
>
{{ loc.name }}
</NuxtLink>
</div>
</template>
```
### Dropdown Language Switcher
```vue
<script setup lang="ts">
const { locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const currentLocale = computed(() =>
locales.value.find((l) => l.code === locale.value),
)
</script>
<template>
<UDropdown>
<template #trigger>
<UButton>
{{ currentLocale?.name }}
</UButton>
</template>
<div class="p-2">
<NuxtLink
v-for="loc of locales"
:key="loc.code"
:to="switchLocalePath(loc.code)"
class="block px-4 py-2 hover:bg-gray-100"
>
{{ loc.name }}
</NuxtLink>
</div>
</UDropdown>
</template>
```
### SEO with I18n
```vue
<script setup lang="ts">
const { locale, locales, t } = useI18n()
const route = useRoute()
const switchLocalePath = useSwitchLocalePath()
useSeoMeta({
title: t("seo.title"),
description: t("seo.description"),
ogLocale: locale.value,
ogTitle: t("seo.ogTitle"),
ogDescription: t("seo.ogDescription"),
})
useHead({
htmlAttrs: {
lang: locale.value,
},
link: [
// Alternate language links for SEO
...locales.value.map((loc) => ({
rel: "alternate",
hreflang: loc.iso,
href: `https://example.com${switchLocalePath(loc.code)}`,
})),
],
})
</script>
```
### Per-Page Translations
```vue
<script setup lang="ts">
const { t } = useI18n({
useScope: "local",
})
</script>
<template>
<div>
<h1>{{ t("title") }}</h1>
<p>{{ t("description") }}</p>
</div>
</template>
<i18n lang="json">
{
"en": {
"title": "About Us",
"description": "Learn more about our company"
},
"fr": {
"title": "À propos",
"description": "En savoir plus sur notre entreprise"
}
}
</i18n>
```
### Dynamic Content Translation
```vue
<script setup lang="ts">
const { data: products } = await useFetch("/api/products")
const { locale } = useI18n()
// Assuming API returns translations
const localizedProducts = computed(() =>
products.value?.map((product) => ({
...product,
name: product.translations[locale.value]?.name || product.name,
description:
product.translations[locale.value]?.description || product.description,
})),
)
</script>
<template>
<div v-for="product of localizedProducts" :key="product.id">
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
</div>
</template>
```
### Lazy Loading Translations
For large translation files:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
lazy: true,
langDir: "locales/",
locales: [
{ code: "en", file: "en.json" },
{ code: "fr", file: "fr.json" },
{ code: "es", file: "es.json" },
],
},
})
```
## Number & Date Formatting
### Number Formatting
Define formats in config:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
numberFormats: {
en: {
currency: {
style: "currency",
currency: "USD",
},
decimal: {
style: "decimal",
minimumFractionDigits: 2,
},
},
fr: {
currency: {
style: "currency",
currency: "EUR",
},
decimal: {
style: "decimal",
minimumFractionDigits: 2,
},
},
},
},
})
```
Usage:
```vue
<template>
<div>
<!-- Currency -->
<p>{{ $n(1234.56, "currency") }}</p>
<!-- en: $1,234.56 -->
<!-- fr: 1 234,56 -->
<!-- Decimal -->
<p>{{ $n(123456.789, "decimal") }}</p>
<!-- en: 123,456.79 -->
<!-- fr: 123 456,79 -->
</div>
</template>
```
### Date Formatting
Define formats in config:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
datetimeFormats: {
en: {
short: {
year: "numeric",
month: "short",
day: "numeric",
},
long: {
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
},
},
fr: {
short: {
year: "numeric",
month: "short",
day: "numeric",
},
long: {
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
},
},
},
},
})
```
Usage:
```vue
<script setup lang="ts">
const date = new Date("2025-01-04")
</script>
<template>
<div>
<!-- Short format -->
<p>{{ $d(date, "short") }}</p>
<!-- en: Jan 4, 2025 -->
<!-- fr: 4 janv. 2025 -->
<!-- Long format -->
<p>{{ $d(date, "long") }}</p>
<!-- en: Saturday, January 4, 2025 -->
<!-- fr: samedi 4 janvier 2025 -->
</div>
</template>
```
## Pluralization
**Translation file:**
```json
{
"items": "no items | one item | {count} items",
"cart": "You have {n} item in your cart | You have {n} items in your cart"
}
```
**Usage:**
```vue
<template>
<div>
<p>{{ $t("items", 0) }}</p>
<!-- "no items" -->
<p>{{ $t("items", 1) }}</p>
<!-- "one item" -->
<p>{{ $t("items", 5) }}</p>
<!-- "5 items" -->
<p>{{ $t("cart", { n: 1 }) }}</p>
<!-- "You have 1 item in your cart" -->
<p>{{ $t("cart", { n: 3 }) }}</p>
<!-- "You have 3 items in your cart" -->
</div>
</template>
```
## TypeScript Support
### Typed Translations
```typescript
// types/i18n.ts
export interface LocaleMessages {
welcome: string
hello: (params: { name: string }) => string
nav: {
home: string
about: string
contact: string
}
}
```
Usage:
```vue
<script setup lang="ts">
import type { LocaleMessages } from "~/types/i18n"
const { t } = useI18n<LocaleMessages>()
// TypeScript will check keys and parameters
const welcome = t("welcome")
const hello = t("hello", { name: "John" })
const navHome = t("nav.home")
</script>
```
## API Routes with I18n
```typescript
// server/api/products.get.ts
export default defineEventHandler(async (event) => {
const locale = getCookie(event, "i18n_redirected") || "en"
const products = await db.products.findMany({
include: {
translations: {
where: { locale },
},
},
})
return products.map((product) => ({
...product,
name: product.translations[0]?.name || product.name,
description: product.translations[0]?.description || product.description,
}))
})
```
## Best Practices
1. **Use prefix_except_default strategy** - Better UX for default locale users
2. **Enable browser detection** - Auto-redirect to user's preferred language
3. **Provide language switcher** - Always visible on every page
4. **Use lazy loading for large apps** - Load translations on demand
5. **Organize translations by feature** - Use nested keys (`nav.home`, `products.title`)
6. **Include locale in SEO** - Use `hreflang` links and `og:locale`
7. **Handle missing translations** - Provide fallback locale
8. **Use placeholders consistently** - `{name}` for simple, `{n}` for pluralization
9. **Test all locales** - Verify layout with longer translations (German, French)
10. **Keep keys consistent** - Same structure across all locale files
## Troubleshooting
### Translations Not Loading
Check:
1. `@nuxtjs/i18n` in `modules` array
2. Locale files in correct `langDir` path
3. File names match `file` property in config
4. JSON is valid (no trailing commas)
### Routes Not Localized
1. Verify `strategy` is set correctly
2. Check `defaultLocale` matches one of your locales
3. Ensure you're using `localePath()` for links
### Browser Detection Not Working
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
detectBrowserLanguage: {
useCookie: true,
cookieKey: "i18n_redirected",
redirectOn: "root",
alwaysRedirect: true,
fallbackLocale: "en",
},
},
})
```
### Missing Translations
Enable warnings in development:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
compilation: {
strictMessage: false,
},
vueI18n: "./i18n.config.ts",
},
})
```
```typescript
// i18n.config.ts
export default {
legacy: false,
locale: "en",
missingWarn: true,
fallbackWarn: true,
}
```
## v10 Changes from v9
### Key Updates
1. **Vue I18n v11** - Upgraded from v10 with JIT compilation as default
2. **Improved Nuxt 4 Support** - Better compatibility with Nuxt 4
3. **Custom Routes** - Use `definePageMeta` for per-page locale configuration
4. **Server-Side Redirects** - Improved server-side redirection behavior
5. **Strict SEO** - Experimental strict SEO head management
6. **Fixed Behaviors** - `strategy` and `redirectOn` combinations now work as expected
### Migration Notes
- `$tc()` API integrated into `$t()` (from Vue I18n v10→v11 upgrade)
- JIT compilation now default (no need for `jit` option)
- New directory structure: i18n files resolved from `<rootDir>/i18n` (configurable with `restructureDir`)
- Context functions require `$` prefix: use `$localePath()` not `localePath()` in templates
## Official Resources
- **Documentation:** https://i18n.nuxtjs.org
- **Migration Guide:** https://i18n.nuxtjs.org/docs/guide/migrating
- **API Reference:** https://i18n.nuxtjs.org/api
- **Vue I18n Docs:** https://vue-i18n.intlify.dev
- **Examples:** https://i18n.nuxtjs.org/examples
- **GitHub:** https://github.com/nuxt-modules/i18n
---
**Note:** This reference covers Nuxt I18n v10 (latest as of 2025-11) with Vue I18n v11. For v9 projects, consult the migration guide.

View File

@@ -0,0 +1,595 @@
# Nuxt Image Reference
**Last Updated:** 2025-11
**Check:** `@nuxt/image` in package.json
Nuxt Image is an image optimization module that provides automatic image optimization, lazy loading, responsive images, and support for multiple image providers.
## Installation & Setup
```bash
pnpm add @nuxt/image
```
**nuxt.config.ts:**
```typescript
export default defineNuxtConfig({
modules: ["@nuxt/image"],
image: {
// Optional configuration
quality: 80,
formats: ["webp", "avif"],
// Image providers
providers: {
cloudinary: {
baseURL: "https://res.cloudinary.com/{your-cloud-name}/image/upload/",
},
},
},
})
```
## Core Components
### NuxtImg
Basic optimized image component:
```vue
<template>
<NuxtImg src="/images/hero.jpg" alt="Hero image" width="800" height="600" />
</template>
```
**Common Props:**
- `src` - Image source (path or URL)
- `alt` - Alt text for accessibility
- `width` / `height` - Dimensions
- `loading` - `"lazy"` (default) or `"eager"`
- `fit` - `"cover"`, `"contain"`, `"fill"`, `"inside"`, `"outside"`
- `format` - `"webp"`, `"avif"`, `"jpg"`, `"png"`
- `quality` - Image quality (0-100)
- `provider` - Image provider to use
### NuxtPicture
Responsive image with multiple formats:
```vue
<template>
<NuxtPicture
src="/images/hero.jpg"
:img-attrs="{ alt: 'Hero image', class: 'rounded-lg' }"
sizes="sm:100vw md:50vw lg:400px"
/>
</template>
```
**Benefits:**
- Automatically generates multiple formats (WebP, AVIF)
- Creates responsive srcset for different screen sizes
- Better browser compatibility with fallbacks
## Common Patterns
### Responsive Images
```vue
<template>
<NuxtPicture
src="/images/product.jpg"
:img-attrs="{ alt: 'Product image' }"
sizes="xs:100vw sm:100vw md:50vw lg:33vw"
:modifiers="{ fit: 'cover' }"
/>
</template>
```
### Image with Loading States
```vue
<template>
<div class="relative">
<NuxtImg
src="/images/large.jpg"
alt="Large image"
loading="lazy"
placeholder
@load="imageLoaded = true"
/>
<div
v-if="!imageLoaded"
class="absolute inset-0 bg-gray-200 animate-pulse"
/>
</div>
</template>
<script setup lang="ts">
const imageLoaded = ref(false)
</script>
```
### Background Images
```vue
<template>
<div
:style="{
backgroundImage: `url(${$img('/images/hero.jpg', { width: 1920, height: 1080 })})`,
backgroundSize: 'cover',
}"
class="h-96"
>
Content
</div>
</template>
```
### Image with Different Formats
```vue
<template>
<NuxtPicture
src="/images/photo.jpg"
format="webp"
:img-attrs="{ alt: 'Photo', class: 'w-full' }"
/>
</template>
```
### Fit Options
```vue
<template>
<div class="grid grid-cols-3 gap-4">
<!-- Cover: Crop to fill dimensions -->
<NuxtImg
src="/images/photo.jpg"
fit="cover"
width="300"
height="300"
alt="Cover"
/>
<!-- Contain: Fit within dimensions -->
<NuxtImg
src="/images/photo.jpg"
fit="contain"
width="300"
height="300"
alt="Contain"
/>
<!-- Fill: Stretch to fill -->
<NuxtImg
src="/images/photo.jpg"
fit="fill"
width="300"
height="300"
alt="Fill"
/>
</div>
</template>
```
## Image Providers
### Local Provider (Default)
For images in `public/` directory:
```vue
<template>
<NuxtImg src="/images/local.jpg" />
</template>
```
### External URLs
```vue
<template>
<NuxtImg src="https://example.com/image.jpg" provider="cloudinary" />
</template>
```
### Cloudinary
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
image: {
cloudinary: {
baseURL: "https://res.cloudinary.com/{your-cloud-name}/image/upload/",
},
},
})
```
```vue
<template>
<NuxtImg provider="cloudinary" src="sample.jpg" width="600" height="400" />
</template>
```
### Vercel / Netlify
Automatically detected and configured when deployed:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
image: {
provider: "vercel", // or 'netlify'
},
})
```
### Custom Provider
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
image: {
providers: {
custom: {
provider: "~/providers/custom-provider.ts",
options: {
baseURL: "https://cdn.example.com",
},
},
},
},
})
```
```typescript
// providers/custom-provider.ts
import { joinURL } from "ufo"
import type { ProviderGetImage } from "@nuxt/image"
export const getImage: ProviderGetImage = (src, { modifiers, baseURL }) => {
const { width, height, format, quality } = modifiers
const url = joinURL(baseURL, src)
return {
url: `${url}?w=${width}&h=${height}&fm=${format}&q=${quality}`,
}
}
```
## Composables
### $img Helper
Generate image URLs programmatically:
```vue
<script setup lang="ts">
const { $img } = useNuxtApp()
// Generate optimized URL
const imageUrl = $img("/images/photo.jpg", {
width: 800,
height: 600,
format: "webp",
quality: 80,
})
// Use in v-bind or computed
const backgroundImage = computed(() =>
$img("/images/hero.jpg", { width: 1920 }),
)
</script>
<template>
<div :style="{ backgroundImage: `url(${backgroundImage})` }">Content</div>
</template>
```
## Performance Optimization
### Lazy Loading (Default)
Images are lazy-loaded by default:
```vue
<template>
<!-- Lazy loaded (default) -->
<NuxtImg src="/images/photo.jpg" loading="lazy" />
<!-- Eager load for above-the-fold images -->
<NuxtImg src="/images/hero.jpg" loading="eager" />
</template>
```
### Preload Critical Images
```vue
<script setup lang="ts">
useHead({
link: [
{
rel: "preload",
as: "image",
href: "/images/hero.jpg",
type: "image/jpeg",
},
],
})
</script>
<template>
<NuxtImg src="/images/hero.jpg" loading="eager" />
</template>
```
### Placeholder / Blur
```vue
<template>
<NuxtImg
src="/images/large.jpg"
placeholder
alt="Image with blur placeholder"
/>
</template>
```
### Image Sizes
Specify responsive sizes for optimal loading:
```vue
<template>
<NuxtPicture
src="/images/responsive.jpg"
sizes="xs:100vw sm:100vw md:50vw lg:400px xl:400px"
:img-attrs="{ alt: 'Responsive image' }"
/>
</template>
```
## Advanced Usage
### Modifiers Object
Pass multiple modifiers:
```vue
<script setup lang="ts">
const imageModifiers = {
width: 800,
height: 600,
fit: "cover",
format: "webp",
quality: 85,
}
</script>
<template>
<NuxtImg src="/images/photo.jpg" :modifiers="imageModifiers" alt="Photo" />
</template>
```
### Dynamic Sources
```vue
<script setup lang="ts">
const images = ref([
{ id: 1, src: "/images/photo1.jpg", alt: "Photo 1" },
{ id: 2, src: "/images/photo2.jpg", alt: "Photo 2" },
{ id: 3, src: "/images/photo3.jpg", alt: "Photo 3" },
])
</script>
<template>
<div class="grid grid-cols-3 gap-4">
<NuxtImg
v-for="image of images"
:key="image.id"
:src="image.src"
:alt="image.alt"
width="400"
height="300"
fit="cover"
/>
</div>
</template>
```
### Image Gallery
```vue
<template>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<NuxtPicture
v-for="(image, index) of gallery"
:key="index"
:src="image.src"
:img-attrs="{
alt: image.alt,
class: 'w-full h-64 object-cover rounded-lg cursor-pointer',
}"
sizes="sm:50vw md:33vw lg:25vw"
@click="openLightbox(image)"
/>
</div>
</template>
<script setup lang="ts">
const gallery = ref([
{ src: "/images/gallery1.jpg", alt: "Gallery 1" },
{ src: "/images/gallery2.jpg", alt: "Gallery 2" },
{ src: "/images/gallery3.jpg", alt: "Gallery 3" },
])
function openLightbox(image: any) {
// Handle lightbox
}
</script>
```
### Art Direction
Different images for different screen sizes:
```vue
<template>
<picture>
<source
media="(min-width: 1024px)"
:srcset="$img('/images/hero-desktop.jpg', { width: 1920 })"
/>
<source
media="(min-width: 768px)"
:srcset="$img('/images/hero-tablet.jpg', { width: 1024 })"
/>
<NuxtImg src="/images/hero-mobile.jpg" alt="Hero" width="768" />
</picture>
</template>
```
## Configuration Reference
### Global Configuration
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
image: {
// Default quality
quality: 80,
// Default formats
formats: ["webp", "avif", "jpg"],
// Image sizes for responsive images
screens: {
xs: 320,
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
xxl: 1536,
"2xl": 1536,
},
// Provider configuration
provider: "cloudinary",
providers: {
cloudinary: {
baseURL: "https://res.cloudinary.com/{cloud-name}/image/upload/",
},
},
// IPX options (local provider)
ipx: {
maxAge: 60 * 60 * 24 * 365, // 1 year cache
},
// Presets
presets: {
avatar: {
modifiers: {
format: "webp",
width: 100,
height: 100,
fit: "cover",
},
},
thumbnail: {
modifiers: {
format: "webp",
width: 300,
height: 200,
fit: "cover",
},
},
},
},
})
```
### Using Presets
```vue
<template>
<NuxtImg src="/images/user.jpg" preset="avatar" alt="User avatar" />
<NuxtImg
src="/images/product.jpg"
preset="thumbnail"
alt="Product thumbnail"
/>
</template>
```
## Best Practices
1. **Always include alt text** - Essential for accessibility
2. **Use NuxtPicture for hero images** - Better format support and responsiveness
3. **Specify dimensions** - Prevents layout shift
4. **Lazy load by default** - Except above-the-fold images
5. **Use appropriate fit** - `cover` for thumbnails, `contain` for products
6. **Optimize quality** - 80-85 is usually sufficient
7. **Leverage providers** - Use CDN providers for external images
8. **Use presets** - Define common image styles once
9. **Test on slow networks** - Verify lazy loading and placeholders work
10. **Prefer WebP/AVIF** - Modern formats for better compression
## Troubleshooting
### Images Not Optimizing
Check:
1. `@nuxt/image` is in `nuxt.config.ts` modules
2. Images are in `public/` directory for local provider
3. Provider is correctly configured
4. Development server was restarted after config changes
### Images Not Loading
1. Verify src path is correct
2. Check provider baseURL configuration
3. Ensure CORS is configured for external images
4. Check network tab for 404/403 errors
### Poor Performance
1. Enable lazy loading (default)
2. Use appropriate image sizes
3. Implement placeholders
4. Use WebP/AVIF formats
5. Configure CDN caching
### Layout Shift
Always specify width and height:
```vue
<template>
<!-- Bad: No dimensions -->
<NuxtImg src="/images/photo.jpg" />
<!-- Good: Dimensions specified -->
<NuxtImg src="/images/photo.jpg" width="800" height="600" />
</template>
```
## Official Resources
- **Documentation:** https://image.nuxt.com
- **Providers:** https://image.nuxt.com/providers
- **API Reference:** https://image.nuxt.com/api
- **GitHub:** https://github.com/nuxt/image
---
**Note:** Always verify provider-specific features with official documentation. Image optimization strategies may vary by provider.

View File

@@ -0,0 +1,161 @@
# Nuxt Modules Reference
Check package.json for installed modules before suggesting module-specific features.
## Image Optimization (@nuxt/image)
**Check:** `@nuxt/image` in package.json
Provides optimized image components with automatic format conversion and responsive images. For comprehensive guidance, see `references/nuxt-image.md`.
## Content Management (@nuxt/content)
**Check:** `@nuxt/content` in package.json
File-based CMS for Nuxt with markdown, YAML, and JSON support. For comprehensive guidance including queries, components, and content navigation, see `references/nuxt-content.md`.
## Icon Management (@nuxt/icon)
**Check:** `@nuxt/icon` in package.json
### Icon Component
```vue
<Icon name="heroicons:home" size="24" />
<Icon name="mdi:github" class="text-2xl" />
```
## UI Components (@nuxt/ui)
**Check:** `@nuxt/ui` in package.json
Provides pre-built components with Tailwind CSS. For comprehensive Nuxt UI guidance including setup, v3/v4 differences, and migration, see `references/nuxt-ui.md`.
## SEO (@nuxtjs/seo)
**Check:** `@nuxtjs/seo` in package.json
Provides SEO utilities including sitemap, robots.txt, and OG images.
```typescript
// Auto-generates sitemap
// Configure in nuxt.config.ts
export default defineNuxtConfig({
site: {
url: "https://example.com",
name: "My Site",
},
})
```
## Internationalization (@nuxtjs/i18n)
**Check:** `@nuxtjs/i18n` in package.json
Provides internationalization with auto-imports, locale routing, and SEO support. For comprehensive guidance including setup, translations, and routing, see `references/nuxt-i18n.md`.
## Tailwind CSS (@nuxtjs/tailwindcss)
**Check:** `@nuxtjs/tailwindcss` in package.json
Auto-configured Tailwind CSS integration. For comprehensive guidance including setup, configuration, dark mode, and best practices, see `references/tailwind.md`.
## Color Mode (@nuxtjs/color-mode)
**Check:** `@nuxtjs/color-mode` in package.json
### Toggle Dark Mode
```vue
<script setup lang="ts">
const colorMode = useColorMode()
function toggleDark() {
colorMode.preference = colorMode.value === "dark" ? "light" : "dark"
}
</script>
<template>
<button @click="toggleDark">
{{ colorMode.value === "dark" ? "🌞" : "🌙" }}
</button>
</template>
```
## ESLint (@nuxt/eslint)
**Check:** `@nuxt/eslint` in package.json
Provides auto-configured ESLint with Nuxt-specific rules. Configuration in `eslint.config.mjs`:
```javascript
import { createConfigForNuxt } from "@nuxt/eslint-config/flat"
export default createConfigForNuxt({
features: {
stylistic: true,
},
})
```
## Security (nuxt-security)
**Check:** `nuxt-security` in package.json
### Security Headers
Automatically applies security headers. Configure in nuxt.config.ts:
```typescript
export default defineNuxtConfig({
security: {
headers: {
contentSecurityPolicy: {
"img-src": ["'self'", "https:", "data:"],
},
},
rateLimiter: {
tokensPerInterval: 150,
interval: "hour",
},
},
})
```
## Fonts (@nuxt/fonts)
**Check:** `@nuxt/fonts` in package.json
Automatically optimizes and loads fonts:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
fonts: {
families: [{ name: "Inter", provider: "google" }],
},
})
```
```vue
<style>
.text {
font-family: "Inter", sans-serif;
}
</style>
```
## Scripts (@nuxt/scripts)
**Check:** `@nuxt/scripts` in package.json
Load third-party scripts efficiently:
```vue
<script setup lang="ts">
useScript({
src: "https://www.googletagmanager.com/gtag/js",
async: true,
})
</script>
```

View File

@@ -0,0 +1,499 @@
# Nuxt Common Patterns
## Data Fetching
### useFetch for API Calls
Use `useFetch` for API endpoints. It runs on both server and client, with automatic hydration.
```typescript
const { data, status, error, refresh } = await useFetch("/api/users")
// With query parameters
const { data, status } = await useFetch("/api/users", {
query: { limit: 10, page: 1 },
})
// With type safety
interface User {
id: number
name: string
}
const { data, status, error } = await useFetch<User[]>("/api/users")
```
**Always handle all states in templates:**
```vue
<template>
<div v-if="status === 'pending'">Loading...</div>
<div v-else-if="status === 'error'">
<p>Error: {{ error?.message }}</p>
</div>
<div v-else-if="data">
<!-- Success state -->
<ul>
<li v-for="user of data" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
```
### useAsyncData for Complex Data
Use `useAsyncData` when you need more control or complex transformations.
```typescript
const { data, status, error } = await useAsyncData("users", async () => {
const users = await $fetch("/api/users")
const stats = await $fetch("/api/stats")
return { users, stats }
})
// With caching key
const { data, status, error } = await useAsyncData(`user-${id}`, () =>
$fetch(`/api/users/${id}`),
)
```
### Lazy Fetching
Use lazy variants when you don't want to block navigation:
```typescript
// Non-blocking
const { status, data } = await useLazyFetch('/api/users')
// Show loading state
<div v-if="status === 'pending'">Loading...</div>
<div v-else>{{ data }}</div>
```
### Client-Only Fetching
```typescript
const { data } = await useFetch("/api/users", {
server: false, // Only fetch on client
})
```
### Refresh and Refetch
```typescript
const { data, status, refresh } = await useFetch("/api/users")
// Manually refetch
await refresh()
// Refetch on event
watch(searchQuery, () => refresh())
```
## SEO and Meta Tags
### useHead
```typescript
useHead({
title: "My Page",
meta: [
{ name: "description", content: "Page description" },
{ property: "og:title", content: "My Page" },
],
link: [{ rel: "canonical", href: "https://example.com/page" }],
})
```
### useSeoMeta (Type-Safe)
```typescript
useSeoMeta({
title: "My Page",
description: "Page description",
ogTitle: "My Page",
ogDescription: "Page description",
ogImage: "https://example.com/image.jpg",
twitterCard: "summary_large_image",
})
```
### definePageMeta
```typescript
definePageMeta({
title: "User Profile",
description: "View user profile",
middleware: ["auth"],
})
```
## Error Handling
### Show Error Page
```typescript
showError({
statusCode: 404,
message: "Page not found",
})
// With custom error
showError({
statusCode: 403,
message: "Access denied",
fatal: true,
})
```
### Clear Error
```typescript
clearError({ redirect: "/" })
```
### Handle Errors in Data Fetching
```typescript
const { data, status, error } = await useFetch("/api/users")
if (error.value) {
showError({
statusCode: error.value.statusCode,
message: error.value.message,
})
}
```
### Error Component
```vue
<!-- error.vue -->
<template>
<div>
<h1>{{ error.statusCode }}</h1>
<p>{{ error.message }}</p>
<button @click="handleError">Go Home</button>
</div>
</template>
<script setup lang="ts">
const { error } = defineProps<{
error: { statusCode: number; message: string }
}>()
function handleError() {
clearError({ redirect: "/" })
}
</script>
```
## Environment Variables and Config
### Runtime Config
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Private (server-only)
apiSecret: process.env.API_SECRET,
databaseUrl: process.env.DATABASE_URL,
// Public (exposed to client)
public: {
apiBase: process.env.API_BASE_URL || "http://localhost:3000",
environment: process.env.NODE_ENV,
},
},
})
// Usage
const config = useRuntimeConfig()
console.log(config.public.apiBase) // Available everywhere
console.log(config.apiSecret) // Server-only
```
### App Config
For non-sensitive configuration that can be updated at runtime:
```typescript
// app.config.ts
export default defineAppConfig({
theme: {
primaryColor: "#3b82f6",
},
})
// Usage
const appConfig = useAppConfig()
console.log(appConfig.theme.primaryColor)
```
## Server API Routes
### GET Request
```typescript
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
return {
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
],
}
})
```
### POST Request
```typescript
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate and save user
return { success: true, user: body }
})
```
### Dynamic Routes
```typescript
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id")
// Fetch user by id
return { id, name: "User" }
})
```
### Error Handling in API Routes
```typescript
export default defineEventHandler(async (event) => {
try {
const data = await fetchData()
return data
} catch (error) {
throw createError({
statusCode: 500,
message: "Internal server error",
})
}
})
```
### Protected API Routes
```typescript
// server/api/admin/users.get.ts
export default defineEventHandler(async (event) => {
const session = await requireUserSession(event)
if (!session.user.isAdmin) {
throw createError({
statusCode: 403,
message: "Forbidden",
})
}
return { users: [] }
})
```
## Middleware
### Route Middleware
```typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useState("user")
if (!user.value) {
return navigateTo("/login")
}
})
// Usage in page
definePageMeta({
middleware: "auth",
})
```
### Global Middleware
```typescript
// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Track page view
console.log("Navigating to:", to.path)
})
```
## State Management
### useState
For shared state across components:
```typescript
// composables/useAuth.ts
export const useAuth = () => {
const user = useState<User | null>("user", () => null)
const isAuthenticated = computed(() => !!user.value)
async function login(credentials: LoginCredentials) {
const response = await $fetch("/api/auth/login", {
method: "POST",
body: credentials,
})
user.value = response.user
}
function logout() {
user.value = null
}
return {
user,
isAuthenticated,
login,
logout,
}
}
// Usage in component
const { user, login, logout } = useAuth()
```
## Composables
### Auto-Import from composables/
```typescript
// composables/useCounter.ts
export const useCounter = () => {
const count = ref(0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
increment,
decrement,
}
}
// Usage (auto-imported)
const { count, increment } = useCounter()
```
## Layouts
### Default Layout
```vue
<!-- layouts/default.vue -->
<template>
<div>
<header>
<nav>Navigation</nav>
</header>
<main>
<slot />
</main>
<footer>Footer</footer>
</div>
</template>
```
### Custom Layout
```vue
<!-- layouts/admin.vue -->
<template>
<div class="admin-layout">
<aside>Sidebar</aside>
<main>
<slot />
</main>
</div>
</template>
<!-- Usage in page -->
<script setup lang="ts">
definePageMeta({
layout: "admin",
})
</script>
```
### Dynamic Layout
```typescript
setPageLayout("admin")
```
## Plugins
### Client-Only Plugin
```typescript
// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
// Only runs on client
console.log("Client-side analytics initialized")
})
```
### Server-Only Plugin
```typescript
// plugins/database.server.ts
export default defineNuxtPlugin(() => {
// Only runs on server
return {
provide: {
db: createDatabaseConnection(),
},
}
})
```
### Universal Plugin
```typescript
// plugins/api.ts
export default defineNuxtPlugin(() => {
const api = $fetch.create({
baseURL: "/api",
onResponseError({ response }) {
if (response.status === 401) {
navigateTo("/login")
}
},
})
return {
provide: {
api,
},
}
})
// Usage
const { $api } = useNuxtApp()
const data = await $api("/users")
```

View File

@@ -0,0 +1,944 @@
# Nuxt UI Reference
**Last Updated:** 2025-11 (v4.1.0)
**Check:** `@nuxt/ui` in package.json
Nuxt UI is a component library built on Tailwind CSS that provides pre-built, accessible UI components for Nuxt applications.
## Version Detection
Check `package.json` to determine which version is installed:
- **v4.x** - Latest version (Nuxt 4 required, Tailwind v4, breaking changes from v3)
- **v3.x** - Previous stable (Nuxt 3, Tailwind v3)
- **v2.x** - Legacy version (deprecated)
For v4-specific features and migration, always verify with official docs: https://ui.nuxt.com
## Installation & Setup
### v4 Setup (Current)
**Prerequisites:**
- Nuxt 4+
- Tailwind CSS v4
```bash
pnpm add @nuxt/ui
```
**Required Configuration:**
1. **nuxt.config.ts:**
```typescript
export default defineNuxtConfig({
modules: ["@nuxt/ui"],
// Optional: Configure color theme
colorMode: {
preference: "system", // 'light' | 'dark' | 'system'
},
})
```
2. **app.vue - UApp Wrapper (Required):**
```vue
<template>
<UApp>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>
```
3. **assets/css/main.css - Tailwind v4 Import:**
```css
@import "tailwindcss";
```
4. **app.config.ts - Color Configuration:**
```typescript
export default defineAppConfig({
ui: {
colors: {
primary: "blue",
neutral: "slate",
},
},
})
```
### v3 Setup (Previous Version)
```bash
pnpm add @nuxt/ui
```
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@nuxt/ui"],
})
```
```css
/* assets/css/main.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
```
## Breaking Changes
### v3 → v4 Migration
**Component Renames:**
- `UButtonGroup``UFieldGroup`
- `UFormGroup``UFieldGroup`
- `UVerticalNavigation``UNavigationTree`
**Modal/Popover/Slideover Structure Changes:**
Major structural changes - trigger button now goes inside component, content uses `#content` slot:
```vue
<!-- v3 -->
<UButton @click="open = true">Open</UButton>
<UModal v-model="open">
<div class="p-4">Content</div>
</UModal>
<!-- v4 -->
<UModal>
<UButton>Open</UButton>
<template #content>
<template #header>Title</template>
<template #body>Content</template>
<template #footer>Footer</template>
</template>
</UModal>
```
**Composable Changes:**
- `useModal()``useOverlay()`
- Overlays now close automatically on close events (no manual `.close()` needed)
**Color System:**
Configure colors in `app.config.ts` instead of component props:
```typescript
// app.config.ts (v4)
export default defineAppConfig({
ui: {
colors: {
primary: "blue", // Changed from 'primary' prop on components
neutral: "slate",
},
},
})
```
**Tailwind v4 Requirement:**
- Must use `@import "tailwindcss"` in main.css
- Tailwind config now uses CSS-based configuration
For complete migration details, always check: https://ui.nuxt.com/getting-started/migration
### v2 → v3 Migration
Major overhaul with many breaking changes. Recommend checking official migration guide for projects still on v2.
## Common Components
### Forms
#### UInput
```vue
<template>
<UInput
v-model="email"
type="email"
placeholder="Enter your email"
icon="i-heroicons-envelope"
/>
</template>
<script setup lang="ts">
const email = ref("")
</script>
```
#### UTextarea
```vue
<template>
<UTextarea
v-model="description"
placeholder="Enter description..."
:rows="4"
/>
</template>
<script setup lang="ts">
const description = ref("")
</script>
```
#### USelect
```vue
<template>
<USelect
v-model="selectedOption"
:options="options"
placeholder="Select an option"
/>
</template>
<script setup lang="ts">
const selectedOption = ref("")
const options = [
{ label: "Option 1", value: "opt1" },
{ label: "Option 2", value: "opt2" },
]
</script>
```
#### UCheckbox & URadio
```vue
<template>
<div>
<UCheckbox v-model="agreed" label="I agree to terms" />
<URadioGroup v-model="plan">
<URadio value="free" label="Free" />
<URadio value="pro" label="Pro" />
</URadioGroup>
</div>
</template>
<script setup lang="ts">
const agreed = ref(false)
const plan = ref("free")
</script>
```
#### UFieldGroup (v4) / UFormGroup (v3)
```vue
<template>
<!-- v4 -->
<UFieldGroup label="Email" description="Your email address" required>
<UInput v-model="email" type="email" />
</UFieldGroup>
<!-- v3 -->
<UFormGroup label="Email" help="Your email address" required>
<UInput v-model="email" type="email" />
</UFormGroup>
</template>
<script setup lang="ts">
const email = ref("")
</script>
```
### Buttons
#### UButton
```vue
<template>
<div class="flex gap-2">
<!-- v4: Color defined in app.config.ts -->
<UButton>Primary Button</UButton>
<UButton variant="outline">Outline</UButton>
<UButton variant="ghost">Ghost</UButton>
<UButton size="sm" icon="i-heroicons-plus">Add</UButton>
<UButton loading>Loading...</UButton>
<UButton disabled>Disabled</UButton>
</div>
</template>
```
#### UButtonGroup (v3) / UFieldGroup (v4)
```vue
<template>
<!-- v3 -->
<UButtonGroup>
<UButton>One</UButton>
<UButton>Two</UButton>
<UButton>Three</UButton>
</UButtonGroup>
<!-- v4: Use flex utilities or UFieldGroup -->
<div
class="flex gap-0 [&>button]:rounded-none [&>button:first-child]:rounded-l [&>button:last-child]:rounded-r"
>
<UButton>One</UButton>
<UButton>Two</UButton>
<UButton>Three</UButton>
</div>
</template>
```
### Overlays
#### UModal
```vue
<template>
<!-- v4: Trigger button inside Modal, content in #content slot -->
<UModal>
<UButton>Open Modal</UButton>
<template #content>
<template #header>
<h3>Modal Title</h3>
</template>
<template #body>
<p>Modal content goes here...</p>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<UButton variant="outline">Cancel</UButton>
<UButton @click="handleSubmit">Submit</UButton>
</div>
</template>
</template>
</UModal>
<!-- v3 (old structure) -->
<div>
<UButton @click="isOpen = true">Open Modal</UButton>
<UModal v-model="isOpen">
<UCard>
<template #header>
<h3>Modal Title</h3>
</template>
<p>Modal content goes here...</p>
<template #footer>
<div class="flex justify-end gap-2">
<UButton variant="outline" @click="isOpen = false">Cancel</UButton>
<UButton @click="handleSubmit">Submit</UButton>
</div>
</template>
</UCard>
</UModal>
</div>
</template>
<script setup lang="ts">
function handleSubmit() {
// Handle submission
}
</script>
```
#### UPopover
```vue
<template>
<!-- v4: Similar structure to Modal -->
<UPopover>
<UButton>Open Popover</UButton>
<template #content>
<div class="p-4">
<p>Popover content</p>
</div>
</template>
</UPopover>
<!-- v3 (old structure) -->
<div>
<UButton @click="isPopoverOpen = true">Open Popover</UButton>
<UPopover v-model="isPopoverOpen">
<div class="p-4">
<p>Popover content</p>
</div>
</UPopover>
</div>
</template>
<script setup lang="ts">
// v4: No state management needed
// v3: const isPopoverOpen = ref(false)
</script>
```
#### UTooltip
```vue
<template>
<UTooltip text="Helpful tooltip">
<UButton icon="i-heroicons-information-circle" />
</UTooltip>
</template>
```
### Navigation
#### ULink
```vue
<template>
<div>
<!-- Internal link -->
<ULink to="/about">About Us</ULink>
<!-- External link -->
<ULink to="https://example.com" target="_blank"> External Link </ULink>
<!-- With icon -->
<ULink to="/settings" icon="i-heroicons-cog"> Settings </ULink>
</div>
</template>
```
#### UTabs
```vue
<template>
<UTabs v-model="selectedTab" :items="tabs">
<template #account>
<div class="p-4">Account settings content</div>
</template>
<template #security>
<div class="p-4">Security settings content</div>
</template>
</UTabs>
</template>
<script setup lang="ts">
const selectedTab = ref("account")
const tabs = [
{ key: "account", label: "Account" },
{ key: "security", label: "Security" },
]
</script>
```
#### UBreadcrumb
```vue
<template>
<UBreadcrumb :items="breadcrumbs" />
</template>
<script setup lang="ts">
const route = useRoute()
const breadcrumbs = computed(() => [
{ label: "Home", to: "/" },
{ label: "Products", to: "/products" },
{ label: route.params.id as string },
])
</script>
```
### Data Display
#### UCard
```vue
<template>
<UCard>
<template #header>
<h3>Card Title</h3>
</template>
<p>Card body content goes here.</p>
<template #footer>
<div class="flex justify-end gap-2">
<UButton>Action</UButton>
</div>
</template>
</UCard>
</template>
```
#### UTable
```vue
<template>
<UTable :columns="columns" :rows="users">
<template #name-data="{ row }">
<span class="font-medium">{{ row.name }}</span>
</template>
<template #actions-data="{ row }">
<UButton size="xs" @click="editUser(row)">Edit</UButton>
</template>
</UTable>
</template>
<script setup lang="ts">
const columns = [
{ key: "name", label: "Name" },
{ key: "email", label: "Email" },
{ key: "actions", label: "Actions" },
]
const users = ref([
{ id: 1, name: "John Doe", email: "john@example.com" },
{ id: 2, name: "Jane Smith", email: "jane@example.com" },
])
function editUser(user: any) {
// Handle edit
}
</script>
```
#### UBadge
```vue
<template>
<div class="flex gap-2">
<UBadge>Default</UBadge>
<UBadge variant="subtle">Subtle</UBadge>
<UBadge size="sm">Small</UBadge>
</div>
</template>
```
#### UAlert
```vue
<template>
<div class="space-y-2">
<UAlert
title="Success"
description="Your changes have been saved."
icon="i-heroicons-check-circle"
/>
<UAlert
title="Error"
description="Something went wrong."
variant="error"
icon="i-heroicons-x-circle"
/>
</div>
</template>
```
### Feedback
#### UNotification (Toast)
```vue
<template>
<UButton @click="showToast">Show Toast</UButton>
</template>
<script setup lang="ts">
const toast = useToast()
function showToast() {
toast.add({
title: "Success",
description: "Your action was successful!",
timeout: 3000,
})
}
</script>
```
#### UProgress
```vue
<template>
<div>
<UProgress :value="progress" :max="100" />
<UButton @click="progress += 10">Increase</UButton>
</div>
</template>
<script setup lang="ts">
const progress = ref(30)
</script>
```
## Form Validation
### Using with Zod (Recommended)
```vue
<template>
<UForm :state="state" :schema="schema" @submit="onSubmit">
<UFieldGroup label="Email" name="email" required>
<UInput v-model="state.email" type="email" />
</UFieldGroup>
<UFieldGroup label="Password" name="password" required>
<UInput v-model="state.password" type="password" />
</UFieldGroup>
<UButton type="submit">Submit</UButton>
</UForm>
</template>
<script setup lang="ts">
import { z } from "zod"
const schema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
})
const state = reactive({
email: "",
password: "",
})
async function onSubmit() {
// Form is validated, handle submission
console.log("Valid form data:", state)
}
</script>
```
## Theming & Customization
### Color Configuration (v4)
```typescript
// app.config.ts
export default defineAppConfig({
ui: {
colors: {
primary: "blue",
secondary: "purple",
success: "green",
warning: "yellow",
error: "red",
neutral: "slate",
},
},
})
```
### Component Styling
Override component styles using Tailwind classes:
```vue
<template>
<UButton
class="custom-button"
:ui="{
base: 'font-bold',
rounded: 'rounded-full',
size: { sm: 'px-4 py-2' },
}"
>
Custom Styled Button
</UButton>
</template>
```
### Dark Mode
Nuxt UI automatically supports dark mode when `@nuxtjs/color-mode` is configured:
```vue
<template>
<div>
<!-- Components automatically adapt to dark mode -->
<UCard>
<p>This card adapts to light/dark mode</p>
</UCard>
<!-- Toggle dark mode -->
<UButton @click="toggleDark"> Toggle Dark Mode </UButton>
</div>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
function toggleDark() {
colorMode.preference = colorMode.value === "dark" ? "light" : "dark"
}
</script>
```
## Nuxt UI-Specific Tailwind Configuration
When using Nuxt UI v4 with Tailwind v4, the Tailwind setup is automatically configured by the module. However, you may need custom configuration:
### Custom Tailwind Classes with Nuxt UI
```css
/* assets/css/main.css */
@import "tailwindcss";
/* Custom utilities that work with Nuxt UI */
@layer utilities {
.text-gradient {
@apply bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent;
}
}
```
### Extending Nuxt UI Colors
```typescript
// tailwind.config.ts
import type { Config } from "tailwindcss"
export default {
theme: {
extend: {
colors: {
// Add custom colors that integrate with Nuxt UI
brand: {
50: "#f0f9ff",
100: "#e0f2fe",
// ... more shades
900: "#0c4a6e",
},
},
},
},
} satisfies Config
```
Reference `app.config.ts` to use custom color in Nuxt UI:
```typescript
// app.config.ts
export default defineAppConfig({
ui: {
colors: {
primary: "brand", // Use custom Tailwind color
},
},
})
```
## Common Patterns
### Loading States
```vue
<template>
<div>
<UButton :loading="isLoading" @click="handleAction"> Submit </UButton>
</div>
</template>
<script setup lang="ts">
const isLoading = ref(false)
async function handleAction() {
isLoading.value = true
try {
await $fetch("/api/action")
} finally {
isLoading.value = false
}
}
</script>
```
### Confirmation Dialogs
```vue
<template>
<!-- v4 structure -->
<UModal>
<UButton>Delete</UButton>
<template #content>
<template #header>
<h3>Confirm Deletion</h3>
</template>
<template #body>
<p>Are you sure you want to delete this item?</p>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<UButton variant="outline">Cancel</UButton>
<UButton variant="error" @click="handleDelete">Delete</UButton>
</div>
</template>
</template>
</UModal>
</template>
<script setup lang="ts">
async function handleDelete() {
// Perform deletion
// Modal closes automatically
}
</script>
```
### Data Tables with Actions
```vue
<template>
<UTable :columns="columns" :rows="items" :loading="pending">
<template #actions-data="{ row }">
<UDropdown :items="getActions(row)">
<UButton icon="i-heroicons-ellipsis-horizontal" variant="ghost" />
</UDropdown>
</template>
</UTable>
</template>
<script setup lang="ts">
const columns = [
{ key: "name", label: "Name" },
{ key: "status", label: "Status" },
{ key: "actions", label: "" },
]
const { data: items, pending } = await useFetch("/api/items")
function getActions(row: any) {
return [
[
{
label: "Edit",
icon: "i-heroicons-pencil",
click: () => editItem(row),
},
],
[
{
label: "Delete",
icon: "i-heroicons-trash",
click: () => deleteItem(row),
},
],
]
}
function editItem(item: any) {
// Handle edit
}
function deleteItem(item: any) {
// Handle delete
}
</script>
```
## Icons
Nuxt UI v4 uses Iconify with the `i-` prefix:
```vue
<template>
<div>
<!-- Heroicons (default icon set) -->
<UButton icon="i-heroicons-home">Home</UButton>
<!-- Material Design Icons -->
<UButton icon="i-mdi-github">GitHub</UButton>
<!-- Font Awesome -->
<UButton icon="i-fa-brands-twitter">Twitter</UButton>
</div>
</template>
```
Browse icons at: https://icones.js.org
## Best Practices
1. **Always wrap app with UApp (v4)** - Required for proper functioning
2. **Use v-model:open for modals** (v4) - Breaking change from v3
3. **Configure colors in app.config.ts** (v4) - Centralized theming
4. **Check package.json version** - Use version-appropriate syntax
5. **Leverage auto-completion** - TypeScript types are fully supported
6. **Use built-in validation** - Integrate with Zod for form validation
7. **Prefer composition** - Use slots and composables over prop overload
8. **Follow Tailwind patterns** - Nuxt UI components accept Tailwind classes
9. **Test dark mode** - Always verify components in both light and dark modes
## Troubleshooting
### Common Issues (v4)
**Missing UApp wrapper:**
```
Error: Nuxt UI components require <UApp> wrapper
```
Solution: Wrap your app in `app.vue`:
```vue
<template>
<UApp>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>
```
**Tailwind not working:**
Ensure `main.css` has:
```css
@import "tailwindcss";
```
**v-model not working on Modal:**
Use `v-model:open` instead of `v-model` in v4.
## Migration Checklist
When upgrading from v3 to v4:
- [ ] Upgrade to Nuxt 4
- [ ] Install Tailwind CSS v4
- [ ] Update `main.css` to use `@import "tailwindcss"`
- [ ] Wrap app with `<UApp>` in `app.vue`
- [ ] Move color config to `app.config.ts`
- [ ] Update `v-model` to `v-model:open` on Modal/Popover/Dialog
- [ ] Rename `UButtonGroup``UFieldGroup`
- [ ] Rename `UFormGroup``UFieldGroup`
- [ ] Rename `UVerticalNavigation``UNavigationTree`
- [ ] Test all components in both light and dark mode
- [ ] Review official migration guide for additional breaking changes
## Official Resources
- **Documentation:** https://ui.nuxt.com
- **Migration Guide:** https://ui.nuxt.com/getting-started/migration
- **Component Reference:** https://ui.nuxt.com/components
- **GitHub:** https://github.com/nuxt/ui
- **Iconify Icons:** https://icones.js.org
---
**Note:** Always verify version-specific syntax with official documentation at https://ui.nuxt.com. This reference covers v3 and v4, but breaking changes may occur in future releases.

View File

@@ -0,0 +1,167 @@
# Pinia State Management
**Check if installed:** Look for `pinia` or `@pinia/nuxt` in package.json before using.
## Overview
Pinia is the official state management library for Vue. In Nuxt, it integrates seamlessly with auto-imports and SSR.
## Store Definition
Use the Composition API style with the setup function pattern.
```typescript
// stores/user.ts
export const useUserStore = defineStore("user", () => {
// State
const user = ref<User | null>(null)
const token = ref<string | null>(null)
// Getters (computed)
const isAuthenticated = computed(() => !!user.value)
const fullName = computed(() => {
if (!user.value) return ""
return `${user.value.firstName} ${user.value.lastName}`
})
// Actions (functions)
async function login(credentials: LoginCredentials) {
const response = await $fetch("/api/auth/login", {
method: "POST",
body: credentials,
})
user.value = response.user
token.value = response.token
}
function logout() {
user.value = null
token.value = null
}
return {
// State
user,
token,
// Getters
isAuthenticated,
fullName,
// Actions
login,
logout,
}
})
```
## Usage in Components
### Accessing Store
```typescript
<script setup lang="ts">
const userStore = useUserStore()
// Direct access (not reactive)
console.log(userStore.user)
// Reactive access using storeToRefs
const { user, isAuthenticated } = storeToRefs(userStore)
// Actions don't need storeToRefs
const { login, logout } = userStore
</script>
```
### Important: storeToRefs
Use `storeToRefs()` to maintain reactivity when destructuring state and getters:
```typescript
// ✅ Correct: Reactive
const { user, isAuthenticated } = storeToRefs(userStore)
// ❌ Wrong: Loses reactivity
const { user, isAuthenticated } = userStore
// ✅ Correct: Actions don't need storeToRefs
const { login, logout } = userStore
```
## SSR Considerations
Pinia stores are automatically hydrated in Nuxt. No special configuration needed for SSR.
### Server-side initialization
```typescript
// stores/config.ts
export const useConfigStore = defineStore('config', () => {
const config = ref<AppConfig | null>(null)
async function fetchConfig() {
// This runs on server during SSR
config.value = await $fetch('/api/config')
}
return { config, fetchConfig }
})
// Usage in page
<script setup lang="ts">
const configStore = useConfigStore()
// Fetch on server, hydrate on client
await configStore.fetchConfig()
</script>
```
## Persisting State
For client-side persistence, use `@pinia-plugin-persistedstate/nuxt`:
```typescript
// stores/preferences.ts
export const usePreferencesStore = defineStore(
"preferences",
() => {
const theme = ref<"light" | "dark">("light")
const language = ref("en")
return { theme, language }
},
{
persist: true, // Persists to localStorage
},
)
```
## Multiple Stores Pattern
Organize stores by domain:
```
stores/
├── user.ts # Authentication & user data
├── cart.ts # Shopping cart
├── products.ts # Product catalog
└── ui.ts # UI state (modals, sidebars, etc.)
```
## Testing Stores
```typescript
import { setActivePinia, createPinia } from "pinia"
import { useUserStore } from "~/stores/user"
describe("User Store", () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it("should authenticate user", async () => {
const store = useUserStore()
await store.login({ email: "test@example.com", password: "password" })
expect(store.isAuthenticated).toBe(true)
})
})
```

View File

@@ -0,0 +1,650 @@
# Tailwind CSS in Nuxt
**Last Updated:** 2025-11 (Tailwind v4.1.16)
**Check:** `@nuxtjs/tailwindcss` in package.json
Tailwind CSS is automatically integrated into Nuxt through the `@nuxtjs/tailwindcss` module, providing utility-first CSS framework support.
## When to Use Tailwind
**Detection:** Always check `package.json` for `@nuxtjs/tailwindcss` dependency before suggesting Tailwind patterns.
**If Tailwind is installed:**
- Prefer Tailwind utility classes in component templates
- Use utilities for layout, spacing, colors, typography, responsive design
- Combine utilities for common patterns (flex, grid, etc.)
**If Tailwind is NOT installed:**
- Use `<style scoped>` for component-specific styles
- Write traditional CSS/SCSS for styling
**Tailwind v4 Capabilities (No `<style>` needed):**
- Custom animations via `@theme` directive with `@keyframes`
- CSS variables for theming via `@theme` with `--color-*`, `--font-*`, etc.
- Scrollbar styling via arbitrary variants: `[&::-webkit-scrollbar]:w-1.5`
- Pseudo-elements via arbitrary variants: `before:content-['★']`
**When to Still Use `<style>` (even with Tailwind v4):**
- Very complex multi-step keyframes that are verbose in `@theme`
- Cross-browser scrollbar styling (Firefox requires different syntax than WebKit)
- Complex pseudo-element content with difficult escaping
- Styles that become unreadable as utility classes (use your judgment)
## Installation & Setup
### Tailwind v4 Setup (Current)
```bash
pnpm add -D tailwindcss @nuxtjs/tailwindcss
```
**nuxt.config.ts:**
```typescript
export default defineNuxtConfig({
modules: ["@nuxtjs/tailwindcss"],
tailwindcss: {
// Optional configuration
exposeConfig: true,
viewer: true, // Enable /_tailwind in dev mode
},
})
```
**assets/css/main.css:**
```css
@import "tailwindcss";
```
**Alternative (explicit layers):**
```css
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
```
### Tailwind v3 Setup (Previous Version)
```bash
pnpm add -D tailwindcss @nuxtjs/tailwindcss
```
**assets/css/main.css:**
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
## Configuration
### tailwind.config.ts
Tailwind v4 uses CSS-based configuration by default, but you can still use TypeScript config:
```typescript
import type { Config } from "tailwindcss"
export default {
content: [
"./components/**/*.{vue,js,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./composables/**/*.{js,ts}",
"./plugins/**/*.{js,ts}",
"./app.vue",
],
theme: {
extend: {
colors: {
primary: {
50: "#f0f9ff",
100: "#e0f2fe",
200: "#bae6fd",
300: "#7dd3fc",
400: "#38bdf8",
500: "#0ea5e9",
600: "#0284c7",
700: "#0369a1",
800: "#075985",
900: "#0c4a6e",
950: "#082f49",
},
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
},
spacing: {
"128": "32rem",
"144": "36rem",
},
},
},
plugins: [],
} satisfies Config
```
### CSS-Based Configuration (v4)
Tailwind v4 supports CSS variables for theming:
```css
/* assets/css/main.css */
@import "tailwindcss";
@layer base {
:root {
--color-primary-50: 240 249 255;
--color-primary-500: 14 165 233;
--color-primary-900: 12 74 110;
}
}
```
## Common Patterns
### Responsive Design
```vue
<template>
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold">
Responsive Heading
</h1>
<!-- Hide on mobile, show on desktop -->
<div class="hidden lg:block">Desktop only content</div>
<!-- Show on mobile, hide on desktop -->
<div class="block lg:hidden">Mobile only content</div>
</div>
</template>
```
### Dark Mode
Nuxt integrates with `@nuxtjs/color-mode` for automatic dark mode support:
```vue
<template>
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<h1 class="text-gray-900 dark:text-gray-100">Auto Dark Mode</h1>
<button
class="bg-blue-500 hover:bg-blue-600 dark:bg-blue-700 dark:hover:bg-blue-800"
@click="toggleDarkMode"
>
Toggle Dark Mode
</button>
</div>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
function toggleDarkMode() {
colorMode.preference = colorMode.value === "dark" ? "light" : "dark"
}
</script>
```
### Custom Components with @apply
```vue
<template>
<button class="btn-primary">Primary Button</button>
</template>
<style>
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white font-medium rounded-lg hover:bg-blue-600 transition-colors;
}
</style>
```
### Layout Utilities
```vue
<template>
<!-- Flexbox -->
<div class="flex items-center justify-between gap-4">
<div>Left</div>
<div>Right</div>
</div>
<!-- Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
<!-- Container -->
<div class="container mx-auto max-w-7xl px-4">Content with max width</div>
</template>
```
### Transitions & Animations
```vue
<template>
<div class="transition-all duration-300 hover:scale-105 hover:shadow-lg">
Hover to animate
</div>
<button class="animate-pulse bg-blue-500 text-white px-4 py-2 rounded">
Pulsing Button
</button>
</template>
```
## Custom Layers
### Adding Custom Utilities
```css
/* assets/css/main.css */
@import "tailwindcss";
@layer utilities {
.text-gradient {
@apply bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
```
Usage:
```vue
<template>
<h1 class="text-gradient text-4xl font-bold">Gradient Text</h1>
<div class="scrollbar-hide overflow-auto">Scrollable without scrollbar</div>
</template>
```
### Custom Components Layer
```css
/* assets/css/main.css */
@import "tailwindcss";
@layer components {
.card {
@apply bg-white dark:bg-gray-800 rounded-lg shadow-md p-6;
}
.card-title {
@apply text-xl font-semibold mb-2 text-gray-900 dark:text-white;
}
.card-body {
@apply text-gray-600 dark:text-gray-300;
}
}
```
Usage:
```vue
<template>
<div class="card">
<h2 class="card-title">Card Title</h2>
<p class="card-body">Card content goes here.</p>
</div>
</template>
```
## Tailwind Plugins
### Using Official Plugins
```bash
pnpm add -D @tailwindcss/forms @tailwindcss/typography @tailwindcss/container-queries
```
```typescript
// tailwind.config.ts
import type { Config } from "tailwindcss"
export default {
plugins: [
require("@tailwindcss/forms"),
require("@tailwindcss/typography"),
require("@tailwindcss/container-queries"),
],
} satisfies Config
```
### @tailwindcss/typography
For rich text content:
```vue
<template>
<article class="prose dark:prose-invert lg:prose-xl mx-auto">
<h1>Article Title</h1>
<p>
Automatically styled rich text content with proper spacing, typography,
and dark mode support.
</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
</ul>
</article>
</template>
```
### @tailwindcss/forms
Automatically styles form elements:
```vue
<template>
<form class="space-y-4">
<!-- Forms plugin provides base styling -->
<input
type="text"
class="form-input rounded-md"
placeholder="Auto-styled input"
/>
<select class="form-select rounded-md">
<option>Option 1</option>
<option>Option 2</option>
</select>
<textarea class="form-textarea rounded-md" rows="4" />
</form>
</template>
```
## Nuxt-Specific Features
### Tailwind Viewer
Enable Tailwind config viewer in development:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
tailwindcss: {
viewer: true,
},
})
```
Access at: `http://localhost:3000/_tailwind`
### IntelliSense Configuration
For VSCode autocomplete, create `.vscode/settings.json`:
```json
{
"tailwindCSS.experimental.classRegex": [
["class:\\s*['\"`]([^'\"`]*)['\"`]", "([^'\"`]*)"],
["class:\\s*{([^}]*)", "['\"`]([^'\"`]*)['\"`]"]
],
"tailwindCSS.includeLanguages": {
"vue": "html",
"typescript": "javascript"
}
}
```
### Scoped Styles with Tailwind
```vue
<template>
<div class="custom-component">Content</div>
</template>
<style scoped>
.custom-component {
@apply bg-blue-500 text-white p-4 rounded-lg;
}
/* Scoped styles work with Tailwind utilities */
.custom-component:hover {
@apply bg-blue-600;
}
</style>
```
## JIT Mode (Always On in v3+)
Just-In-Time compilation is default in Tailwind v3+. Benefits:
- **Arbitrary values:** `w-[137px]`, `top-[117px]`
- **Arbitrary variants:** `[&>*]:text-red-500`
- **Dynamic values:** `bg-[#1da1f2]`
```vue
<template>
<div>
<!-- Arbitrary values -->
<div class="w-[137px] h-[91px] bg-[#1da1f2]">Custom size and color</div>
<!-- Arbitrary variants -->
<ul class="[&>li]:text-blue-500 [&>li]:font-bold">
<li>Item 1</li>
<li>Item 2</li>
</ul>
<!-- Dynamic spacing -->
<div class="m-[calc(100%-3rem)]">Calculated margin</div>
</div>
</template>
```
## Performance Optimization
### Purging Unused Styles
Automatically handled by Tailwind. Ensure `content` paths in config are correct:
```typescript
// tailwind.config.ts
export default {
content: [
"./components/**/*.{vue,js,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
],
} satisfies Config
```
### Safelist Dynamic Classes
For dynamically generated classes:
```typescript
// tailwind.config.ts
export default {
safelist: [
"bg-red-500",
"bg-blue-500",
"bg-green-500",
{
pattern: /bg-(red|blue|green)-(400|500|600)/,
},
],
} satisfies Config
```
Or use string interpolation with full class names:
```vue
<script setup lang="ts">
// Bad: Classes may be purged
const color = ref("blue")
const bgClass = computed(() => `bg-${color.value}-500`)
// Good: Full class names are detected
const bgClass = computed(() => {
if (color.value === "blue") return "bg-blue-500"
if (color.value === "red") return "bg-red-500"
return "bg-green-500"
})
</script>
```
## Common Utilities Reference
### Spacing
- `p-{size}` - padding
- `m-{size}` - margin
- `space-x-{size}` - horizontal gap between children
- `space-y-{size}` - vertical gap between children
- `gap-{size}` - gap in flex/grid
### Sizing
- `w-{size}` - width
- `h-{size}` - height
- `max-w-{size}` - max width
- `min-h-{size}` - min height
### Typography
- `text-{size}` - font size
- `font-{weight}` - font weight
- `leading-{size}` - line height
- `tracking-{size}` - letter spacing
- `text-{align}` - text alignment
### Colors
- `text-{color}-{shade}` - text color
- `bg-{color}-{shade}` - background color
- `border-{color}-{shade}` - border color
### Layout
- `flex`, `inline-flex` - flexbox
- `grid`, `inline-grid` - grid
- `block`, `inline-block`, `hidden` - display
- `relative`, `absolute`, `fixed`, `sticky` - positioning
### Borders
- `border`, `border-{width}` - border width
- `rounded-{size}` - border radius
- `border-{side}` - specific side border
### Effects
- `shadow-{size}` - box shadow
- `opacity-{amount}` - opacity
- `blur-{amount}` - blur effect
## Best Practices
1. **Use Tailwind viewer in dev** - Enable `viewer: true` to explore your config
2. **Avoid @apply overuse** - Prefer composition over extraction for most cases
3. **Use arbitrary values sparingly** - Stick to design system values when possible
4. **Leverage dark mode utilities** - Always test with `dark:` variants
5. **Keep config minimal** - Only extend when design system requires it
6. **Use safelist for dynamic classes** - Or ensure full class names in templates
7. **Enable IntelliSense** - Configure VSCode for autocomplete
8. **Responsive-first** - Use mobile-first breakpoints (`sm:`, `md:`, `lg:`)
9. **Use Tailwind plugins** - Leverage official plugins for common needs
10. **Avoid inline styles** - Prefer Tailwind utilities over style attributes
## Troubleshooting
### Styles Not Applied
Check:
1. `@import "tailwindcss"` in `main.css` (v4) or `@tailwind` directives (v3)
2. `main.css` imported in `nuxt.config.ts` or `app.vue`
3. `content` paths in `tailwind.config.ts` include all component files
4. Class names are complete strings (not dynamically concatenated)
### Dark Mode Not Working
Ensure `@nuxtjs/color-mode` is installed:
```bash
pnpm add @nuxtjs/color-mode
```
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@nuxtjs/tailwindcss", "@nuxtjs/color-mode"],
})
```
### IntelliSense Not Working
1. Install "Tailwind CSS IntelliSense" VSCode extension
2. Create `.vscode/settings.json` with Tailwind config
3. Restart VSCode
### Build Size Too Large
1. Verify `content` paths only include necessary files
2. Remove unused plugins
3. Avoid safelisting large pattern sets
4. Use CDN for development, bundled for production
## Migration from v3 to v4
Key changes when upgrading to Tailwind v4:
1. **Import syntax:**
```css
/* v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* v4 */
@import "tailwindcss";
```
2. **CSS-based configuration** - Use CSS variables alongside TypeScript config
3. **Removed PostCSS plugins** - Many features now built-in
4. **New color system** - Improved color utilities with better composability
Always consult official Tailwind v4 migration guide for comprehensive changes.
## Official Resources
- **Tailwind CSS Docs:** https://tailwindcss.com
- **Nuxt Tailwind Module:** https://tailwindcss.nuxtjs.org
- **Cheat Sheet:** https://nerdcave.com/tailwind-cheat-sheet
- **Component Examples:** https://tailwindui.com (official, paid)
- **Community Components:** https://tailwindcomponents.com
---
**Note:** Always verify version-specific features with official documentation. This guide covers both Tailwind v3 and v4, focusing on Nuxt integration.

View File

@@ -0,0 +1,238 @@
# Vue Component Best Practices
These patterns apply to Vue 3+ and modern Nuxt applications.
## Script Setup Syntax
ALWAYS use `<script setup lang="ts">` for component script sections.
### Props
- ALWAYS use TypeScript type-based syntax for `defineProps()` instead of runtime `PropType` declarations
- ALWAYS destructure props directly from `defineProps()` to maintain reactivity and enable inline defaults
- If no props are used in the script section, call `defineProps()` without destructuring
```typescript
// ✅ Correct: Type-based with destructuring and inline defaults
<script setup lang="ts">
const { title, count = 0, enabled = true } = defineProps<{
title: string
count?: number
enabled?: boolean
}>()
</script>
// ✅ Correct: No props used in script
<script setup lang="ts">
defineProps<{
title: string
}>()
</script>
// ❌ Wrong: Runtime PropType syntax
import type { PropType } from 'vue'
defineProps({
items: {
type: Array as PropType<string[]>,
required: true
}
})
```
### Emits
ALWAYS use type-based syntax for `defineEmits` in TypeScript instead of runtime array syntax.
```typescript
// ✅ Correct: Type-based emits
const emit = defineEmits<{
update: [value: string]
close: []
}>()
// ❌ Wrong: Runtime array syntax
const emit = defineEmits(["update", "close"])
```
### Event Handler Typing
When emitting events with event objects, use appropriate event types:
```typescript
// ✅ Correct: Typed event handlers
const emit = defineEmits<{
click: [event: MouseEvent]
keypress: [event: KeyboardEvent]
input: [event: InputEvent]
submit: [event: SubmitEvent]
}>()
// Usage in template
<button @click="emit('click', $event)">Click me</button>
<input @keypress="emit('keypress', $event)" />
```
### v-model
USE `defineModel()` for v-model implementations instead of manually defining props and emits.
```typescript
// ✅ Correct: Using defineModel
const modelValue = defineModel<string>()
// ❌ Wrong: Manual prop + emit
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ "update:modelValue": [value: string] }>()
```
## Component Structure
### Template Placement
ALWAYS place the `<template>` section at the top of Vue SFC files, before `<script>` and `<style>` sections.
```vue
<!-- Correct order -->
<template>
<div>{{ title }}</div>
</template>
<script setup lang="ts">
const { title } = defineProps<{ title: string }>()
</script>
<style scoped>
div {
color: blue;
}
</style>
```
### Component Naming
- ALWAYS use multi-word component names except for Nuxt pages and layouts
- Examples: `UserProfile.vue`, `SearchBar.vue` (not `User.vue`, `Search.vue`)
- Exception: `pages/index.vue`, `pages/about.vue`, `layouts/default.vue` are acceptable
## Template Directives
### v-for Loops
- ALWAYS use `key` in v-for loops
- ALWAYS use `v-for="item of items"` instead of `v-for="item in items"` to match JavaScript `for...of` syntax
```vue
<!-- Correct -->
<li v-for="user of users" :key="user.id">{{ user.name }}</li>
<!-- Wrong: Missing key -->
<li v-for="user of users">{{ user.name }}</li>
<!-- Wrong: Using 'in' instead of 'of' -->
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
```
### Prop Binding Shorthand
ALWAYS use shorthand syntax (`:propName`) when passing a prop with the same name as the variable, instead of verbose form (`:propName="propName"`).
```vue
<!-- Correct: Shorthand -->
<UserCard :username :avatar :bio />
<!-- Wrong: Verbose when unnecessary -->
<UserCard :username="username" :avatar="avatar" :bio="bio" />
```
## Reactivity and State
### Reactive References
PREFER `ref()` for reactive state instead of `reactive()`.
```typescript
// ✅ Preferred: Using ref
const count = ref(0)
const user = ref({ name: "Alice", age: 30 })
// ❌ Less preferred: Using reactive
const state = reactive({ count: 0 })
```
### VueUse Composables
PREFER VueUse composables and utility functions over custom implementations for common tasks like state management, DOM interactions, and browser APIs.
Check if `@vueuse/core` or `@vueuse/nuxt` is installed before suggesting VueUse composables.
```typescript
// ✅ Preferred: Using VueUse (if installed)
import { useLocalStorage, useMouse, useWindowSize } from "@vueuse/core"
const token = useLocalStorage("auth-token", "")
// ❌ Avoid: Custom implementation when VueUse exists
const token = ref(localStorage.getItem("auth-token") || "")
watch(token, (val) => localStorage.setItem("auth-token", val))
```
## Component Organization
### Logical Grouping
PREFER to group by logical concerns rather than grouping by type (data, methods, computed) within components. Keep related state, computed properties, and functions together.
```typescript
// ✅ Preferred: Grouped by feature/concern
<script setup lang="ts">
// User authentication concern
const user = ref(null)
const isAuthenticated = computed(() => !!user.value)
async function login() { /* ... */ }
// Search functionality concern
const searchQuery = ref('')
const searchResults = computed(() => /* ... */)
function handleSearch() { /* ... */ }
</script>
// ❌ Less preferred: Grouped by type
<script setup lang="ts">
// All refs
const user = ref(null)
const searchQuery = ref('')
// All computed
const isAuthenticated = computed(() => !!user.value)
const searchResults = computed(() => /* ... */)
// All functions
async function login() { /* ... */ }
function handleSearch() { /* ... */ }
</script>
```
## Styling
ALWAYS use `<style scoped>` for component-specific styles. PREFER short, simple class names - scoped styles eliminate need for BEM or complex naming:
```vue
<!-- Preferred -->
<template>
<div class="card">
<div class="header">Title</div>
<div class="content">Body</div>
</div>
</template>
<style scoped>
.card { padding: 1rem; }
.header { font-weight: bold; }
.content { flex: 1; }
</style>
<!-- Avoid: BEM with scoped styles -->
<div class="user-card">
<div class="user-card__header">Title</div>
<div class="user-card__content">Body</div>
</div>
```

View File

@@ -0,0 +1,244 @@
# VueUse Integration
**Check if installed:** Look for `@vueuse/core` or `@vueuse/nuxt` in package.json before using.
## Overview
VueUse is a collection of essential Vue Composition Utilities. It provides ready-made composables for common tasks, eliminating the need to write custom implementations.
## Installation Detection
Prefer VueUse over custom implementations when it's installed. Always check package.json first.
## Common Composables
### State Management
**useLocalStorage / useSessionStorage**
```typescript
// Syncs ref with localStorage
const token = useLocalStorage("auth-token", "")
const user = useLocalStorage<User | null>("user", null)
// Automatically saves to localStorage when updated
token.value = "new-token"
```
**useToggle**
```typescript
const [isOpen, toggle] = useToggle()
// toggle() switches between true/false
// toggle(true) sets to true
// toggle(false) sets to false
```
**useCounter**
```typescript
const { count, inc, dec, set, reset } = useCounter(0, { min: 0, max: 10 })
```
### Browser APIs
**useMouse**
```typescript
const { x, y, sourceType } = useMouse()
// Reactive mouse position
```
**useWindowSize**
```typescript
const { width, height } = useWindowSize()
// Reactive window dimensions
```
**useScroll**
```typescript
const { x, y, isScrolling, arrivedState, directions } = useScroll(elementRef)
// Track scroll position and state
```
**useClipboard**
```typescript
const { text, copy, copied, isSupported } = useClipboard()
async function copyToClipboard() {
await copy("Hello World")
// copied.value is true for 1.5s
}
```
**useMediaQuery**
```typescript
const isMobile = useMediaQuery("(max-width: 768px)")
const isDark = useMediaQuery("(prefers-color-scheme: dark)")
```
### Element Interaction
**useIntersectionObserver**
```typescript
const target = ref<HTMLElement | null>(null)
const isVisible = ref(false)
useIntersectionObserver(target, ([{ isIntersecting }]) => {
isVisible.value = isIntersecting
})
```
**useElementVisibility**
```typescript
const target = ref<HTMLElement | null>(null)
const isVisible = useElementVisibility(target)
```
**useFocus**
```typescript
const input = ref<HTMLInputElement | null>(null)
const { focused } = useFocus(input)
// Auto-focus on mount
useFocus(input, { initialValue: true })
```
### Network
**useFetch** (VueUse version, different from Nuxt's useFetch)
```typescript
const { data, error, isFetching } = useFetch("https://api.example.com/users")
.get()
.json()
```
Note: In Nuxt, prefer Nuxt's built-in `useFetch` for API calls. Use VueUse's `useFetch` only for external APIs or when you need different behavior.
### Utilities
**refDebounced / useDebounceFn**
```typescript
// Debounce a ref value
const input = ref("")
const debounced = refDebounced(input, 500)
// Debounce a function
const debouncedFn = useDebounceFn(() => {
console.log("Debounced!")
}, 500)
```
**refThrottled / useThrottleFn**
```typescript
// Throttle a ref value
const scrollY = ref(0)
const throttled = refThrottled(scrollY, 200)
// Throttle a function
const throttledFn = useThrottleFn(() => {
console.log("Throttled!")
}, 200)
```
**useInterval / useTimeout**
```typescript
const counter = ref(0)
const { pause, resume } = useInterval(1000, {
callback: () => counter.value++,
})
```
**useEventListener**
```typescript
useEventListener(window, "resize", () => {
console.log("Window resized")
})
// On element
const button = ref<HTMLButtonElement | null>(null)
useEventListener(button, "click", () => {
console.log("Clicked")
})
```
## Nuxt Auto-Import
When using `@vueuse/nuxt`, composables are auto-imported. No need to import them manually.
```typescript
// ✅ With @vueuse/nuxt
const { width } = useWindowSize()
// ❌ Without @vueuse/nuxt (need manual import)
import { useWindowSize } from "@vueuse/core"
const { width } = useWindowSize()
```
## Common Patterns
### Dark Mode Toggle
```typescript
const isDark = useDark()
const toggleDark = useToggle(isDark)
```
### Infinite Scroll
```typescript
const el = ref<HTMLElement | null>(null)
const { arrivedState } = useScroll(el)
watch(
() => arrivedState.bottom,
async (isBottom) => {
if (isBottom) {
await loadMore()
}
},
)
```
### Form Field Debouncing
```typescript
const searchQuery = ref("")
const debouncedQuery = refDebounced(searchQuery, 300)
watch(debouncedQuery, async (query) => {
// API call with debounced value
await searchProducts(query)
})
```
### Responsive Breakpoints
```typescript
const breakpoints = useBreakpoints({
mobile: 640,
tablet: 768,
laptop: 1024,
desktop: 1280,
})
const isMobile = breakpoints.smaller("tablet")
const isDesktop = breakpoints.greaterOrEqual("laptop")
```
## Resources
- [VueUse Documentation](https://vueuse.org/)
- [Function Reference](https://vueuse.org/functions.html)