Initial commit
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user