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