Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
|
||||||
488
commands/prime/components.md
Normal file
488
commands/prime/components.md
Normal 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
552
commands/prime/framework.md
Normal 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
97
plugin.lock.json
Normal 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
313
skills/nuxt/SKILL.md
Normal 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
|
||||||
313
skills/nuxt/references/drizzle-db0.md
Normal file
313
skills/nuxt/references/drizzle-db0.md
Normal 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
|
||||||
|
})
|
||||||
|
```
|
||||||
827
skills/nuxt/references/nuxt-content.md
Normal file
827
skills/nuxt/references/nuxt-content.md
Normal 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.
|
||||||
757
skills/nuxt/references/nuxt-i18n.md
Normal file
757
skills/nuxt/references/nuxt-i18n.md
Normal 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.
|
||||||
595
skills/nuxt/references/nuxt-image.md
Normal file
595
skills/nuxt/references/nuxt-image.md
Normal 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.
|
||||||
161
skills/nuxt/references/nuxt-modules.md
Normal file
161
skills/nuxt/references/nuxt-modules.md
Normal 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>
|
||||||
|
```
|
||||||
499
skills/nuxt/references/nuxt-patterns.md
Normal file
499
skills/nuxt/references/nuxt-patterns.md
Normal 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")
|
||||||
|
```
|
||||||
944
skills/nuxt/references/nuxt-ui.md
Normal file
944
skills/nuxt/references/nuxt-ui.md
Normal 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.
|
||||||
167
skills/nuxt/references/pinia.md
Normal file
167
skills/nuxt/references/pinia.md
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
650
skills/nuxt/references/tailwind.md
Normal file
650
skills/nuxt/references/tailwind.md
Normal 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.
|
||||||
238
skills/nuxt/references/vue-best-practices.md
Normal file
238
skills/nuxt/references/vue-best-practices.md
Normal 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>
|
||||||
|
```
|
||||||
244
skills/nuxt/references/vueuse.md
Normal file
244
skills/nuxt/references/vueuse.md
Normal 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)
|
||||||
Reference in New Issue
Block a user