format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
This commit is contained in:
@@ -3,7 +3,7 @@ name: nuxt
|
||||
description: Nuxt full-stack Vue framework with SSR, auto-imports, and file-based routing. Use when working with Nuxt apps, server routes, useFetch, middleware, or hybrid rendering.
|
||||
metadata:
|
||||
author: Anthony Fu
|
||||
version: "2026.1.28"
|
||||
version: '2026.1.28'
|
||||
source: Generated from https://github.com/nuxt/nuxt, scripts located at https://github.com/antfu/skills
|
||||
---
|
||||
|
||||
@@ -13,43 +13,43 @@ Nuxt is a full-stack Vue framework that provides server-side rendering, file-bas
|
||||
|
||||
## Core
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Directory Structure | Project folder structure, conventions, file organization | [core-directory-structure](references/core-directory-structure.md) |
|
||||
| Configuration | nuxt.config.ts, app.config.ts, runtime config, environment variables | [core-config](references/core-config.md) |
|
||||
| CLI Commands | Dev server, build, generate, preview, and utility commands | [core-cli](references/core-cli.md) |
|
||||
| Routing | File-based routing, dynamic routes, navigation, middleware, layouts | [core-routing](references/core-routing.md) |
|
||||
| Data Fetching | useFetch, useAsyncData, $fetch, caching, refresh | [core-data-fetching](references/core-data-fetching.md) |
|
||||
| Modules | Creating and using Nuxt modules, Nuxt Kit utilities | [core-modules](references/core-modules.md) |
|
||||
| Deployment | Platform-agnostic deployment with Nitro, Vercel, Netlify, Cloudflare | [core-deployment](references/core-deployment.md) |
|
||||
| Topic | Description | Reference |
|
||||
| ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Directory Structure | Project folder structure, conventions, file organization | [core-directory-structure](references/core-directory-structure.md) |
|
||||
| Configuration | nuxt.config.ts, app.config.ts, runtime config, environment variables | [core-config](references/core-config.md) |
|
||||
| CLI Commands | Dev server, build, generate, preview, and utility commands | [core-cli](references/core-cli.md) |
|
||||
| Routing | File-based routing, dynamic routes, navigation, middleware, layouts | [core-routing](references/core-routing.md) |
|
||||
| Data Fetching | useFetch, useAsyncData, $fetch, caching, refresh | [core-data-fetching](references/core-data-fetching.md) |
|
||||
| Modules | Creating and using Nuxt modules, Nuxt Kit utilities | [core-modules](references/core-modules.md) |
|
||||
| Deployment | Platform-agnostic deployment with Nitro, Vercel, Netlify, Cloudflare | [core-deployment](references/core-deployment.md) |
|
||||
|
||||
## Features
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Composables Auto-imports | Vue APIs, Nuxt composables, custom composables, utilities | [features-composables](references/features-composables.md) |
|
||||
| Components Auto-imports | Component naming, lazy loading, hydration strategies | [features-components-autoimport](references/features-components-autoimport.md) |
|
||||
| Built-in Components | NuxtLink, NuxtPage, NuxtLayout, ClientOnly, and more | [features-components](references/features-components.md) |
|
||||
| State Management | useState composable, SSR-friendly state, Pinia integration | [features-state](references/features-state.md) |
|
||||
| Server Routes | API routes, server middleware, Nitro server engine | [features-server](references/features-server.md) |
|
||||
| Topic | Description | Reference |
|
||||
| ------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| Composables Auto-imports | Vue APIs, Nuxt composables, custom composables, utilities | [features-composables](references/features-composables.md) |
|
||||
| Components Auto-imports | Component naming, lazy loading, hydration strategies | [features-components-autoimport](references/features-components-autoimport.md) |
|
||||
| Built-in Components | NuxtLink, NuxtPage, NuxtLayout, ClientOnly, and more | [features-components](references/features-components.md) |
|
||||
| State Management | useState composable, SSR-friendly state, Pinia integration | [features-state](references/features-state.md) |
|
||||
| Server Routes | API routes, server middleware, Nitro server engine | [features-server](references/features-server.md) |
|
||||
|
||||
## Rendering
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Topic | Description | Reference |
|
||||
| --------------- | ----------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| Rendering Modes | Universal (SSR), client-side (SPA), hybrid rendering, route rules | [rendering-modes](references/rendering-modes.md) |
|
||||
|
||||
## Best Practices
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Data Fetching Patterns | Efficient fetching, caching, parallel requests, error handling | [best-practices-data-fetching](references/best-practices-data-fetching.md) |
|
||||
| SSR & Hydration | Avoiding context leaks, hydration mismatches, composable patterns | [best-practices-ssr](references/best-practices-ssr.md) |
|
||||
| Topic | Description | Reference |
|
||||
| ---------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| Data Fetching Patterns | Efficient fetching, caching, parallel requests, error handling | [best-practices-data-fetching](references/best-practices-data-fetching.md) |
|
||||
| SSR & Hydration | Avoiding context leaks, hydration mismatches, composable patterns | [best-practices-ssr](references/best-practices-ssr.md) |
|
||||
|
||||
## Advanced
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Layers | Extending applications with reusable layers | [advanced-layers](references/advanced-layers.md) |
|
||||
| Lifecycle Hooks | Build-time, runtime, and server hooks | [advanced-hooks](references/advanced-hooks.md) |
|
||||
| Topic | Description | Reference |
|
||||
| ---------------- | ----------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| Layers | Extending applications with reusable layers | [advanced-layers](references/advanced-layers.md) |
|
||||
| Lifecycle Hooks | Build-time, runtime, and server hooks | [advanced-hooks](references/advanced-hooks.md) |
|
||||
| Module Authoring | Creating publishable Nuxt modules with Nuxt Kit | [advanced-module-authoring](references/advanced-module-authoring.md) |
|
||||
|
||||
@@ -18,7 +18,7 @@ Used in `nuxt.config.ts` or modules:
|
||||
export default defineNuxtConfig({
|
||||
hooks: {
|
||||
'build:before': () => {
|
||||
console.log('Build starting...')
|
||||
console.log('Build starting...');
|
||||
},
|
||||
'pages:extend': (pages) => {
|
||||
// Add custom pages
|
||||
@@ -26,14 +26,14 @@ export default defineNuxtConfig({
|
||||
name: 'custom',
|
||||
path: '/custom',
|
||||
file: '~/pages/custom.vue',
|
||||
})
|
||||
});
|
||||
},
|
||||
'components:dirs': (dirs) => {
|
||||
// Add component directories
|
||||
dirs.push({ path: '~/extra-components' })
|
||||
dirs.push({ path: '~/extra-components' });
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### In Modules
|
||||
@@ -43,35 +43,35 @@ export default defineNuxtConfig({
|
||||
export default defineNuxtModule({
|
||||
setup(options, nuxt) {
|
||||
nuxt.hook('ready', async (nuxt) => {
|
||||
console.log('Nuxt is ready')
|
||||
})
|
||||
console.log('Nuxt is ready');
|
||||
});
|
||||
|
||||
nuxt.hook('close', async (nuxt) => {
|
||||
console.log('Nuxt is closing')
|
||||
})
|
||||
console.log('Nuxt is closing');
|
||||
});
|
||||
|
||||
nuxt.hook('modules:done', () => {
|
||||
console.log('All modules loaded')
|
||||
})
|
||||
console.log('All modules loaded');
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Common Build Hooks
|
||||
|
||||
| Hook | When |
|
||||
|------|------|
|
||||
| `ready` | Nuxt initialization complete |
|
||||
| `close` | Nuxt is closing |
|
||||
| `modules:done` | All modules installed |
|
||||
| `build:before` | Before build starts |
|
||||
| `build:done` | Build complete |
|
||||
| `pages:extend` | Pages routes resolved |
|
||||
| `components:dirs` | Component dirs being resolved |
|
||||
| `imports:extend` | Auto-imports being resolved |
|
||||
| `nitro:config` | Before Nitro config finalized |
|
||||
| `vite:extend` | Vite context created |
|
||||
| `vite:extendConfig` | Before Vite config finalized |
|
||||
| Hook | When |
|
||||
| ------------------- | ----------------------------- |
|
||||
| `ready` | Nuxt initialization complete |
|
||||
| `close` | Nuxt is closing |
|
||||
| `modules:done` | All modules installed |
|
||||
| `build:before` | Before build starts |
|
||||
| `build:done` | Build complete |
|
||||
| `pages:extend` | Pages routes resolved |
|
||||
| `components:dirs` | Component dirs being resolved |
|
||||
| `imports:extend` | Auto-imports being resolved |
|
||||
| `nitro:config` | Before Nitro config finalized |
|
||||
| `vite:extend` | Vite context created |
|
||||
| `vite:extendConfig` | Before Vite config finalized |
|
||||
|
||||
## App Hooks (Runtime)
|
||||
|
||||
@@ -83,54 +83,54 @@ Used in plugins and composables:
|
||||
// plugins/lifecycle.ts
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook('app:created', (vueApp) => {
|
||||
console.log('Vue app created')
|
||||
})
|
||||
console.log('Vue app created');
|
||||
});
|
||||
|
||||
nuxtApp.hook('app:mounted', (vueApp) => {
|
||||
console.log('App mounted')
|
||||
})
|
||||
console.log('App mounted');
|
||||
});
|
||||
|
||||
nuxtApp.hook('page:start', () => {
|
||||
console.log('Page navigation starting')
|
||||
})
|
||||
console.log('Page navigation starting');
|
||||
});
|
||||
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
console.log('Page navigation finished')
|
||||
})
|
||||
console.log('Page navigation finished');
|
||||
});
|
||||
|
||||
nuxtApp.hook('page:loading:start', () => {
|
||||
console.log('Page loading started')
|
||||
})
|
||||
console.log('Page loading started');
|
||||
});
|
||||
|
||||
nuxtApp.hook('page:loading:end', () => {
|
||||
console.log('Page loading ended')
|
||||
})
|
||||
})
|
||||
console.log('Page loading ended');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Common App Hooks
|
||||
|
||||
| Hook | When |
|
||||
|------|------|
|
||||
| `app:created` | Vue app created |
|
||||
| `app:mounted` | Vue app mounted (client only) |
|
||||
| `app:error` | Fatal error occurred |
|
||||
| `page:start` | Page navigation starting |
|
||||
| `page:finish` | Page navigation finished |
|
||||
| Hook | When |
|
||||
| -------------------- | ----------------------------- |
|
||||
| `app:created` | Vue app created |
|
||||
| `app:mounted` | Vue app mounted (client only) |
|
||||
| `app:error` | Fatal error occurred |
|
||||
| `page:start` | Page navigation starting |
|
||||
| `page:finish` | Page navigation finished |
|
||||
| `page:loading:start` | Loading indicator should show |
|
||||
| `page:loading:end` | Loading indicator should hide |
|
||||
| `link:prefetch` | Link is being prefetched |
|
||||
| `page:loading:end` | Loading indicator should hide |
|
||||
| `link:prefetch` | Link is being prefetched |
|
||||
|
||||
### Using Runtime Hooks
|
||||
|
||||
```ts
|
||||
// composables/usePageTracking.ts
|
||||
export function usePageTracking() {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const nuxtApp = useNuxtApp();
|
||||
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
trackPageView(useRoute().path)
|
||||
})
|
||||
trackPageView(useRoute().path);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -143,37 +143,37 @@ Used in server plugins:
|
||||
export default defineNitroPlugin((nitroApp) => {
|
||||
// Modify HTML before sending
|
||||
nitroApp.hooks.hook('render:html', (html, { event }) => {
|
||||
html.head.push('<meta name="custom" content="value">')
|
||||
html.bodyAppend.push('<script>console.log("injected")</script>')
|
||||
})
|
||||
html.head.push('<meta name="custom" content="value">');
|
||||
html.bodyAppend.push('<script>console.log("injected")</script>');
|
||||
});
|
||||
|
||||
// Modify response
|
||||
nitroApp.hooks.hook('render:response', (response, { event }) => {
|
||||
console.log('Sending response:', response.statusCode)
|
||||
})
|
||||
console.log('Sending response:', response.statusCode);
|
||||
});
|
||||
|
||||
// Before request
|
||||
nitroApp.hooks.hook('request', (event) => {
|
||||
console.log('Request:', event.path)
|
||||
})
|
||||
console.log('Request:', event.path);
|
||||
});
|
||||
|
||||
// After response
|
||||
nitroApp.hooks.hook('afterResponse', (event) => {
|
||||
console.log('Response sent')
|
||||
})
|
||||
})
|
||||
console.log('Response sent');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Common Nitro Hooks
|
||||
|
||||
| Hook | When |
|
||||
|------|------|
|
||||
| `request` | Request received |
|
||||
| `beforeResponse` | Before sending response |
|
||||
| `afterResponse` | After response sent |
|
||||
| `render:html` | Before HTML is sent |
|
||||
| Hook | When |
|
||||
| ----------------- | ---------------------------- |
|
||||
| `request` | Request received |
|
||||
| `beforeResponse` | Before sending response |
|
||||
| `afterResponse` | After response sent |
|
||||
| `render:html` | Before HTML is sent |
|
||||
| `render:response` | Before response is finalized |
|
||||
| `error` | Error occurred |
|
||||
| `error` | Error occurred |
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
@@ -181,23 +181,23 @@ export default defineNitroPlugin((nitroApp) => {
|
||||
|
||||
```ts
|
||||
// types/hooks.d.ts
|
||||
import type { HookResult } from '@nuxt/schema'
|
||||
import type { HookResult } from '@nuxt/schema';
|
||||
|
||||
declare module '#app' {
|
||||
interface RuntimeNuxtHooks {
|
||||
'my-app:event': (data: MyEventData) => HookResult
|
||||
'my-app:event': (data: MyEventData) => HookResult;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@nuxt/schema' {
|
||||
interface NuxtHooks {
|
||||
'my-module:init': () => HookResult
|
||||
'my-module:init': () => HookResult;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'nitropack/types' {
|
||||
interface NitroRuntimeHooks {
|
||||
'my-server:event': (data: any) => void
|
||||
'my-server:event': (data: any) => void;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -208,15 +208,15 @@ declare module 'nitropack/types' {
|
||||
// In a plugin
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
// Call custom hook
|
||||
nuxtApp.callHook('my-app:event', { type: 'custom' })
|
||||
})
|
||||
nuxtApp.callHook('my-app:event', { type: 'custom' });
|
||||
});
|
||||
|
||||
// In a module
|
||||
export default defineNuxtModule({
|
||||
setup(options, nuxt) {
|
||||
nuxt.callHook('my-module:init')
|
||||
nuxt.callHook('my-module:init');
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## useRuntimeHook
|
||||
@@ -227,8 +227,8 @@ Call hooks at runtime from components:
|
||||
<script setup lang="ts">
|
||||
// Register a callback for a runtime hook
|
||||
useRuntimeHook('app:error', (error) => {
|
||||
console.error('App error:', error)
|
||||
})
|
||||
console.error('App error:', error);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -240,13 +240,13 @@ useRuntimeHook('app:error', (error) => {
|
||||
// plugins/analytics.client.ts
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
const route = useRoute()
|
||||
const route = useRoute();
|
||||
analytics.track('pageview', {
|
||||
path: route.path,
|
||||
title: document.title,
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
@@ -254,17 +254,17 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
```ts
|
||||
// plugins/performance.client.ts
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
let navigationStart: number
|
||||
let navigationStart: number;
|
||||
|
||||
nuxtApp.hook('page:start', () => {
|
||||
navigationStart = performance.now()
|
||||
})
|
||||
navigationStart = performance.now();
|
||||
});
|
||||
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
const duration = performance.now() - navigationStart
|
||||
console.log(`Navigation took ${duration}ms`)
|
||||
})
|
||||
})
|
||||
const duration = performance.now() - navigationStart;
|
||||
console.log(`Navigation took ${duration}ms`);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Inject HTML
|
||||
@@ -277,12 +277,12 @@ export default defineNitroPlugin((nitroApp) => {
|
||||
<script>
|
||||
window.APP_CONFIG = ${JSON.stringify(config)}
|
||||
</script>
|
||||
`)
|
||||
})
|
||||
})
|
||||
`);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/guide/going-further/hooks
|
||||
- https://nuxt.com/docs/api/advanced/hooks
|
||||
|
||||
@@ -14,11 +14,8 @@ Layers allow sharing and reusing partial Nuxt applications across projects. They
|
||||
```ts
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
extends: [
|
||||
'@my-org/base-layer',
|
||||
'@nuxtjs/ui-layer',
|
||||
],
|
||||
})
|
||||
extends: ['@my-org/base-layer', '@nuxtjs/ui-layer'],
|
||||
});
|
||||
```
|
||||
|
||||
### From Git Repository
|
||||
@@ -34,7 +31,7 @@ export default defineNuxtConfig({
|
||||
'gitlab:username/repo',
|
||||
'bitbucket:username/repo',
|
||||
],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### From Local Directory
|
||||
@@ -42,11 +39,8 @@ export default defineNuxtConfig({
|
||||
```ts
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
extends: [
|
||||
'../base-layer',
|
||||
'./layers/shared',
|
||||
],
|
||||
})
|
||||
extends: ['../base-layer', './layers/shared'],
|
||||
});
|
||||
```
|
||||
|
||||
### Auto-scanned Layers
|
||||
@@ -95,7 +89,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
// Shared modules
|
||||
modules: ['@nuxt/ui'],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Layer Components
|
||||
@@ -122,15 +116,16 @@ Use in consuming project:
|
||||
```ts
|
||||
// my-layer/app/composables/useTheme.ts
|
||||
export function useTheme() {
|
||||
const isDark = useState('theme-dark', () => false)
|
||||
const toggle = () => isDark.value = !isDark.value
|
||||
return { isDark, toggle }
|
||||
const isDark = useState('theme-dark', () => false);
|
||||
const toggle = () => (isDark.value = !isDark.value);
|
||||
return { isDark, toggle };
|
||||
}
|
||||
```
|
||||
|
||||
## Layer Priority
|
||||
|
||||
Override order (highest to lowest):
|
||||
|
||||
1. Your project files
|
||||
2. Auto-scanned layers (alphabetically, Z > A)
|
||||
3. `extends` array (first > last)
|
||||
@@ -149,7 +144,7 @@ Access layer files:
|
||||
|
||||
```ts
|
||||
// Auto-scanned layers get aliases
|
||||
import Component from '#layers/base/components/Component.vue'
|
||||
import Component from '#layers/base/components/Component.vue';
|
||||
```
|
||||
|
||||
Named aliases:
|
||||
@@ -160,12 +155,12 @@ export default defineNuxtConfig({
|
||||
$meta: {
|
||||
name: 'my-layer',
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// In consuming project
|
||||
import { something } from '#layers/my-layer/utils'
|
||||
import { something } from '#layers/my-layer/utils';
|
||||
```
|
||||
|
||||
## Publishing Layers
|
||||
@@ -201,16 +196,14 @@ export GIGET_AUTH=<github-token>
|
||||
|
||||
```ts
|
||||
// my-layer/nuxt.config.ts
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const currentDir = dirname(fileURLToPath(import.meta.url))
|
||||
const currentDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineNuxtConfig({
|
||||
css: [
|
||||
join(currentDir, './assets/main.css'),
|
||||
],
|
||||
})
|
||||
css: [join(currentDir, './assets/main.css')],
|
||||
});
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
@@ -218,10 +211,8 @@ export default defineNuxtConfig({
|
||||
```ts
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
extends: [
|
||||
['github:user/layer', { install: true }],
|
||||
],
|
||||
})
|
||||
extends: [['github:user/layer', { install: true }]],
|
||||
});
|
||||
```
|
||||
|
||||
### Disable Layer Modules
|
||||
@@ -233,7 +224,7 @@ export default defineNuxtConfig({
|
||||
// Disable modules from layer
|
||||
image: false, // Disables @nuxt/image
|
||||
pinia: false, // Disables @pinia/nuxt
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Starter Template
|
||||
@@ -265,7 +256,7 @@ theme-layer/
|
||||
// theme-layer/nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
css: ['~/assets/theme.css'],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
@@ -275,24 +266,24 @@ export default defineAppConfig({
|
||||
primaryColor: '#00dc82',
|
||||
darkMode: false,
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// consuming-app/nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
extends: ['theme-layer'],
|
||||
})
|
||||
});
|
||||
|
||||
// consuming-app/app/app.config.ts
|
||||
export default defineAppConfig({
|
||||
theme: {
|
||||
primaryColor: '#ff0000', // Override
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/getting-started/layers
|
||||
- https://nuxt.com/docs/guide/going-further/layers
|
||||
|
||||
@@ -31,12 +31,12 @@ my-nuxt-module/
|
||||
|
||||
```ts
|
||||
// src/module.ts
|
||||
import { defineNuxtModule, createResolver, addPlugin, addComponent, addImports } from '@nuxt/kit'
|
||||
import { defineNuxtModule, createResolver, addPlugin, addComponent, addImports } from '@nuxt/kit';
|
||||
|
||||
export interface ModuleOptions {
|
||||
prefix?: string
|
||||
apiKey: string
|
||||
enabled?: boolean
|
||||
prefix?: string;
|
||||
apiKey: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
@@ -52,13 +52,13 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
enabled: true,
|
||||
},
|
||||
setup(options, nuxt) {
|
||||
if (!options.enabled) return
|
||||
if (!options.enabled) return;
|
||||
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Module setup logic here
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Using `.with()` for Strict Type Inference
|
||||
@@ -66,12 +66,12 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
When you need TypeScript to infer that default values are always present:
|
||||
|
||||
```ts
|
||||
import { defineNuxtModule } from '@nuxt/kit'
|
||||
import { defineNuxtModule } from '@nuxt/kit';
|
||||
|
||||
interface ModuleOptions {
|
||||
apiKey: string
|
||||
baseURL: string
|
||||
timeout?: number
|
||||
apiKey: string;
|
||||
baseURL: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>().with({
|
||||
@@ -87,7 +87,7 @@ export default defineNuxtModule<ModuleOptions>().with({
|
||||
// resolvedOptions.baseURL is guaranteed to be string (not undefined)
|
||||
// resolvedOptions.timeout is guaranteed to be number (not undefined)
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Adding Runtime Assets
|
||||
@@ -95,63 +95,63 @@ export default defineNuxtModule<ModuleOptions>().with({
|
||||
### Components
|
||||
|
||||
```ts
|
||||
import { addComponent, addComponentsDir, createResolver } from '@nuxt/kit'
|
||||
import { addComponent, addComponentsDir, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Single component
|
||||
addComponent({
|
||||
name: 'MyButton',
|
||||
filePath: resolve('./runtime/components/MyButton.vue'),
|
||||
})
|
||||
});
|
||||
|
||||
// Component directory with prefix
|
||||
addComponentsDir({
|
||||
path: resolve('./runtime/components'),
|
||||
prefix: 'My',
|
||||
pathPrefix: false,
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Composables and Auto-imports
|
||||
|
||||
```ts
|
||||
import { addImports, addImportsDir, createResolver } from '@nuxt/kit'
|
||||
import { addImports, addImportsDir, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Single import
|
||||
addImports({
|
||||
name: 'useMyUtil',
|
||||
from: resolve('./runtime/composables/useMyUtil'),
|
||||
})
|
||||
});
|
||||
|
||||
// Directory of composables
|
||||
addImportsDir(resolve('./runtime/composables'))
|
||||
addImportsDir(resolve('./runtime/composables'));
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Plugins
|
||||
|
||||
```ts
|
||||
import { addPlugin, addPluginTemplate, createResolver } from '@nuxt/kit'
|
||||
import { addPlugin, addPluginTemplate, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup(options) {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Static plugin file
|
||||
addPlugin({
|
||||
src: resolve('./runtime/plugins/myPlugin'),
|
||||
mode: 'client', // 'client', 'server', or 'all'
|
||||
})
|
||||
});
|
||||
|
||||
// Dynamic plugin with generated code
|
||||
addPluginTemplate({
|
||||
@@ -166,9 +166,9 @@ export default defineNuxtPlugin({
|
||||
// Plugin logic
|
||||
}
|
||||
})`,
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Server Extensions
|
||||
@@ -176,67 +176,67 @@ export default defineNuxtPlugin({
|
||||
### Server Handlers
|
||||
|
||||
```ts
|
||||
import { addServerHandler, addServerScanDir, createResolver } from '@nuxt/kit'
|
||||
import { addServerHandler, addServerScanDir, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Single handler
|
||||
addServerHandler({
|
||||
route: '/api/my-endpoint',
|
||||
handler: resolve('./runtime/server/api/my-endpoint'),
|
||||
})
|
||||
});
|
||||
|
||||
// Scan entire server directory (api/, routes/, middleware/, utils/)
|
||||
addServerScanDir(resolve('./runtime/server'))
|
||||
addServerScanDir(resolve('./runtime/server'));
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Server Composables
|
||||
|
||||
```ts
|
||||
import { addServerImports, addServerImportsDir, createResolver } from '@nuxt/kit'
|
||||
import { addServerImports, addServerImportsDir, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Single server import
|
||||
addServerImports({
|
||||
name: 'useServerUtil',
|
||||
from: resolve('./runtime/server/utils/useServerUtil'),
|
||||
})
|
||||
});
|
||||
|
||||
// Server composables directory
|
||||
addServerImportsDir(resolve('./runtime/server/composables'))
|
||||
addServerImportsDir(resolve('./runtime/server/composables'));
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Nitro Plugin
|
||||
|
||||
```ts
|
||||
import { addServerPlugin, createResolver } from '@nuxt/kit'
|
||||
import { addServerPlugin, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
addServerPlugin(resolve('./runtime/server/plugin'))
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
addServerPlugin(resolve('./runtime/server/plugin'));
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// runtime/server/plugin.ts
|
||||
import { defineNitroPlugin } from 'nitropack/runtime'
|
||||
import { defineNitroPlugin } from 'nitropack/runtime';
|
||||
|
||||
export default defineNitroPlugin((nitroApp) => {
|
||||
nitroApp.hooks.hook('request', (event) => {
|
||||
console.log('Request:', event.path)
|
||||
})
|
||||
})
|
||||
console.log('Request:', event.path);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Templates and Virtual Files
|
||||
@@ -244,17 +244,17 @@ export default defineNitroPlugin((nitroApp) => {
|
||||
### Generate Virtual Files
|
||||
|
||||
```ts
|
||||
import { addTemplate, addTypeTemplate, addServerTemplate, createResolver } from '@nuxt/kit'
|
||||
import { addTemplate, addTypeTemplate, addServerTemplate, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup(options, nuxt) {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Client/build virtual file (accessible via #build/my-config.mjs)
|
||||
addTemplate({
|
||||
filename: 'my-config.mjs',
|
||||
getContents: () => `export default ${JSON.stringify(options)}`,
|
||||
})
|
||||
});
|
||||
|
||||
// Type declarations
|
||||
addTypeTemplate({
|
||||
@@ -265,15 +265,15 @@ declare module '#my-module' {
|
||||
apiKey: string
|
||||
}
|
||||
}`,
|
||||
})
|
||||
});
|
||||
|
||||
// Nitro virtual file (accessible in server routes)
|
||||
addServerTemplate({
|
||||
filename: '#my-module/config.mjs',
|
||||
getContents: () => `export const config = ${JSON.stringify(options)}`,
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Access Virtual Files
|
||||
@@ -281,20 +281,20 @@ declare module '#my-module' {
|
||||
```ts
|
||||
// In runtime plugin
|
||||
// @ts-expect-error - virtual file
|
||||
import config from '#build/my-config.mjs'
|
||||
import config from '#build/my-config.mjs';
|
||||
|
||||
// In server routes
|
||||
import { config } from '#my-module/config.js'
|
||||
import { config } from '#my-module/config.js';
|
||||
```
|
||||
|
||||
## Extending Pages and Routes
|
||||
|
||||
```ts
|
||||
import { extendPages, extendRouteRules, addRouteMiddleware, createResolver } from '@nuxt/kit'
|
||||
import { extendPages, extendRouteRules, addRouteMiddleware, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Add pages
|
||||
extendPages((pages) => {
|
||||
@@ -302,22 +302,22 @@ export default defineNuxtModule({
|
||||
name: 'my-page',
|
||||
path: '/my-route',
|
||||
file: resolve('./runtime/pages/MyPage.vue'),
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// Add route rules (caching, redirects, etc.)
|
||||
extendRouteRules('/api/**', {
|
||||
cache: { maxAge: 60 },
|
||||
})
|
||||
});
|
||||
|
||||
// Add middleware
|
||||
addRouteMiddleware({
|
||||
name: 'my-middleware',
|
||||
path: resolve('./runtime/middleware/myMiddleware'),
|
||||
global: true,
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Module Dependencies
|
||||
@@ -351,7 +351,7 @@ export default defineNuxtModule({
|
||||
setup() {
|
||||
// Dependencies are guaranteed to be set up before this runs
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Dynamic Dependencies
|
||||
@@ -382,16 +382,16 @@ export default defineNuxtModule({
|
||||
},
|
||||
onInstall(nuxt) {
|
||||
// First-time setup
|
||||
console.log('Module installed for the first time')
|
||||
console.log('Module installed for the first time');
|
||||
},
|
||||
onUpgrade(nuxt, options, previousVersion) {
|
||||
// Version upgrade migrations
|
||||
console.log(`Upgrading from ${previousVersion}`)
|
||||
console.log(`Upgrading from ${previousVersion}`);
|
||||
},
|
||||
setup(options, nuxt) {
|
||||
// Regular setup runs every build
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Extending Configuration
|
||||
@@ -400,22 +400,22 @@ export default defineNuxtModule({
|
||||
export default defineNuxtModule({
|
||||
setup(options, nuxt) {
|
||||
// Add CSS
|
||||
nuxt.options.css.push('my-module/styles.css')
|
||||
nuxt.options.css.push('my-module/styles.css');
|
||||
|
||||
// Add runtime config
|
||||
nuxt.options.runtimeConfig.public.myModule = {
|
||||
apiUrl: options.apiUrl,
|
||||
}
|
||||
};
|
||||
|
||||
// Extend Vite config
|
||||
nuxt.options.vite.optimizeDeps ||= {}
|
||||
nuxt.options.vite.optimizeDeps.include ||= []
|
||||
nuxt.options.vite.optimizeDeps.include.push('some-package')
|
||||
nuxt.options.vite.optimizeDeps ||= {};
|
||||
nuxt.options.vite.optimizeDeps.include ||= [];
|
||||
nuxt.options.vite.optimizeDeps.include.push('some-package');
|
||||
|
||||
// Add build transpile
|
||||
nuxt.options.build.transpile.push('my-package')
|
||||
nuxt.options.build.transpile.push('my-package');
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Using Hooks
|
||||
@@ -425,7 +425,7 @@ export default defineNuxtModule({
|
||||
// Declarative hooks
|
||||
hooks: {
|
||||
'components:dirs': (dirs) => {
|
||||
dirs.push({ path: '~/extra' })
|
||||
dirs.push({ path: '~/extra' });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -433,45 +433,42 @@ export default defineNuxtModule({
|
||||
// Programmatic hooks
|
||||
nuxt.hook('pages:extend', (pages) => {
|
||||
// Modify pages
|
||||
})
|
||||
});
|
||||
|
||||
nuxt.hook('imports:extend', (imports) => {
|
||||
imports.push({ name: 'myHelper', from: 'my-package' })
|
||||
})
|
||||
imports.push({ name: 'myHelper', from: 'my-package' });
|
||||
});
|
||||
|
||||
nuxt.hook('nitro:config', (config) => {
|
||||
// Modify Nitro config
|
||||
})
|
||||
});
|
||||
|
||||
nuxt.hook('vite:extendConfig', (config) => {
|
||||
// Modify Vite config
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Path Resolution
|
||||
|
||||
```ts
|
||||
import { createResolver, resolvePath, findPath } from '@nuxt/kit'
|
||||
import { createResolver, resolvePath, findPath } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
async setup(options, nuxt) {
|
||||
// Resolver relative to module
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
const pluginPath = resolve('./runtime/plugin')
|
||||
const pluginPath = resolve('./runtime/plugin');
|
||||
|
||||
// Resolve with extensions and aliases
|
||||
const entrypoint = await resolvePath('@some/package')
|
||||
const entrypoint = await resolvePath('@some/package');
|
||||
|
||||
// Find first existing file
|
||||
const configPath = await findPath([
|
||||
resolve('./config.ts'),
|
||||
resolve('./config.js'),
|
||||
])
|
||||
const configPath = await findPath([resolve('./config.ts'), resolve('./config.js')]);
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Module Package.json
|
||||
@@ -520,7 +517,7 @@ export default defineNuxtConfig({
|
||||
myModule: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@@ -9,12 +9,12 @@ Effective data fetching patterns for SSR-friendly, performant Nuxt applications.
|
||||
|
||||
## Choose the Right Tool
|
||||
|
||||
| Scenario | Use |
|
||||
|----------|-----|
|
||||
| Component initial data | `useFetch` or `useAsyncData` |
|
||||
| User interactions (clicks, forms) | `$fetch` |
|
||||
| Third-party SDK/API | `useAsyncData` with custom function |
|
||||
| Multiple parallel requests | `useAsyncData` with `Promise.all` |
|
||||
| Scenario | Use |
|
||||
| --------------------------------- | ----------------------------------- |
|
||||
| Component initial data | `useFetch` or `useAsyncData` |
|
||||
| User interactions (clicks, forms) | `$fetch` |
|
||||
| Third-party SDK/API | `useAsyncData` with custom function |
|
||||
| Multiple parallel requests | `useAsyncData` with `Promise.all` |
|
||||
|
||||
## Await vs Non-Await Usage
|
||||
|
||||
@@ -25,7 +25,7 @@ The `await` keyword controls whether data fetching **blocks navigation**:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Navigation waits until data is fetched (uses Vue Suspense)
|
||||
const { data } = await useFetch('/api/posts')
|
||||
const { data } = await useFetch('/api/posts');
|
||||
// data.value is available immediately after this line
|
||||
</script>
|
||||
```
|
||||
@@ -39,7 +39,7 @@ const { data } = await useFetch('/api/posts')
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Navigation proceeds immediately, data fetches in background
|
||||
const { data, status } = useFetch('/api/posts', { lazy: true })
|
||||
const { data, status } = useFetch('/api/posts', { lazy: true });
|
||||
// data.value may be undefined initially - check status!
|
||||
</script>
|
||||
|
||||
@@ -53,17 +53,17 @@ Equivalent to using `useLazyFetch`:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data, status } = useLazyFetch('/api/posts')
|
||||
const { data, status } = useLazyFetch('/api/posts');
|
||||
</script>
|
||||
```
|
||||
|
||||
### When to Use Each
|
||||
|
||||
| Pattern | Use Case |
|
||||
|---------|----------|
|
||||
| `await useFetch()` | Critical data needed for SEO/initial render |
|
||||
| Pattern | Use Case |
|
||||
| -------------------------- | ----------------------------------------------- |
|
||||
| `await useFetch()` | Critical data needed for SEO/initial render |
|
||||
| `useFetch({ lazy: true })` | Non-critical data, better perceived performance |
|
||||
| `await useLazyFetch()` | Same as lazy, await only ensures initialization |
|
||||
| `await useLazyFetch()` | Same as lazy, await only ensures initialization |
|
||||
|
||||
## Avoid Double Fetching
|
||||
|
||||
@@ -72,7 +72,7 @@ const { data, status } = useLazyFetch('/api/posts')
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// This fetches TWICE: once on server, once on client
|
||||
const data = await $fetch('/api/posts')
|
||||
const data = await $fetch('/api/posts');
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -81,7 +81,7 @@ const data = await $fetch('/api/posts')
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Fetches on server, hydrates on client (no double fetch)
|
||||
const { data } = await useFetch('/api/posts')
|
||||
const { data } = await useFetch('/api/posts');
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -92,7 +92,7 @@ const { data } = await useFetch('/api/posts')
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Key is auto-generated from file/line - can cause issues
|
||||
const { data } = await useAsyncData(() => fetchPosts())
|
||||
const { data } = await useAsyncData(() => fetchPosts());
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -101,17 +101,13 @@ const { data } = await useAsyncData(() => fetchPosts())
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Explicit key for predictable caching
|
||||
const { data } = await useAsyncData(
|
||||
'posts',
|
||||
() => fetchPosts(),
|
||||
)
|
||||
const { data } = await useAsyncData('posts', () => fetchPosts());
|
||||
|
||||
// Dynamic keys for parameterized data
|
||||
const route = useRoute()
|
||||
const { data: post } = await useAsyncData(
|
||||
`post-${route.params.id}`,
|
||||
() => fetchPost(route.params.id),
|
||||
)
|
||||
const route = useRoute();
|
||||
const { data: post } = await useAsyncData(`post-${route.params.id}`, () =>
|
||||
fetchPost(route.params.id),
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -119,7 +115,7 @@ const { data: post } = await useAsyncData(
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data, status, error } = await useFetch('/api/posts')
|
||||
const { data, status, error } = await useFetch('/api/posts');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -139,18 +135,18 @@ const { data, status, error } = await useFetch('/api/posts')
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const id = useRoute().params.id
|
||||
const id = useRoute().params.id;
|
||||
|
||||
// Critical data - blocks navigation
|
||||
const { data: post } = await useFetch(`/api/posts/${id}`)
|
||||
const { data: post } = await useFetch(`/api/posts/${id}`);
|
||||
|
||||
// Non-critical data - doesn't block navigation
|
||||
const { data: comments, status } = useFetch(`/api/posts/${id}/comments`, {
|
||||
lazy: true,
|
||||
})
|
||||
});
|
||||
|
||||
// Or use useLazyFetch
|
||||
const { data: related } = useLazyFetch(`/api/posts/${id}/related`)
|
||||
const { data: related } = useLazyFetch(`/api/posts/${id}/related`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -173,7 +169,7 @@ const { data: related } = useLazyFetch(`/api/posts/${id}/related`)
|
||||
const { data } = await useFetch('/api/users', {
|
||||
// Only include these fields in payload
|
||||
pick: ['id', 'name', 'avatar'],
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -183,14 +179,14 @@ const { data } = await useFetch('/api/users', {
|
||||
<script setup lang="ts">
|
||||
const { data } = await useFetch('/api/posts', {
|
||||
transform: (posts) => {
|
||||
return posts.map(post => ({
|
||||
return posts.map((post) => ({
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
excerpt: post.content.slice(0, 100),
|
||||
date: new Date(post.createdAt).toLocaleDateString(),
|
||||
}))
|
||||
}));
|
||||
},
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -200,17 +196,14 @@ const { data } = await useFetch('/api/posts', {
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data } = await useAsyncData(
|
||||
'dashboard',
|
||||
async (_nuxtApp, { signal }) => {
|
||||
const [user, posts, stats] = await Promise.all([
|
||||
$fetch('/api/user', { signal }),
|
||||
$fetch('/api/posts', { signal }),
|
||||
$fetch('/api/stats', { signal }),
|
||||
])
|
||||
return { user, posts, stats }
|
||||
},
|
||||
)
|
||||
const { data } = await useAsyncData('dashboard', async (_nuxtApp, { signal }) => {
|
||||
const [user, posts, stats] = await Promise.all([
|
||||
$fetch('/api/user', { signal }),
|
||||
$fetch('/api/posts', { signal }),
|
||||
$fetch('/api/stats', { signal }),
|
||||
]);
|
||||
return { user, posts, stats };
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -222,7 +215,7 @@ const { data } = await useAsyncData(
|
||||
const [{ data: user }, { data: posts }] = await Promise.all([
|
||||
useFetch('/api/user'),
|
||||
useFetch('/api/posts'),
|
||||
])
|
||||
]);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -232,14 +225,14 @@ const [{ data: user }, { data: posts }] = await Promise.all([
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const page = ref(1)
|
||||
const category = ref('all')
|
||||
const page = ref(1);
|
||||
const category = ref('all');
|
||||
|
||||
const { data } = await useFetch('/api/posts', {
|
||||
query: { page, category },
|
||||
// Auto-refresh when these change
|
||||
watch: [page, category],
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -247,10 +240,10 @@ const { data } = await useFetch('/api/posts', {
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data, refresh, status } = await useFetch('/api/posts')
|
||||
const { data, refresh, status } = await useFetch('/api/posts');
|
||||
|
||||
async function refreshPosts() {
|
||||
await refresh()
|
||||
await refresh();
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -259,16 +252,16 @@ async function refreshPosts() {
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const userId = ref<string | null>(null)
|
||||
const userId = ref<string | null>(null);
|
||||
|
||||
const { data, execute } = useFetch(() => `/api/users/${userId.value}`, {
|
||||
immediate: false, // Don't fetch until userId is set
|
||||
})
|
||||
});
|
||||
|
||||
// Later, when userId is available
|
||||
function loadUser(id: string) {
|
||||
userId.value = id
|
||||
execute()
|
||||
userId.value = id;
|
||||
execute();
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -282,7 +275,7 @@ const { data } = await useFetch('/api/static-content', {
|
||||
server: true,
|
||||
lazy: true,
|
||||
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key],
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -290,15 +283,19 @@ const { data } = await useFetch('/api/static-content', {
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data, error, refresh } = await useFetch('/api/posts')
|
||||
const { data, error, refresh } = await useFetch('/api/posts');
|
||||
|
||||
// Watch for errors if need event-like handling
|
||||
watch(error, (err) => {
|
||||
if (err) {
|
||||
console.error('Fetch failed:', err)
|
||||
// Show toast, redirect, etc.
|
||||
}
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
error,
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error('Fetch failed:', err);
|
||||
// Show toast, redirect, etc.
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -314,16 +311,16 @@ watch(error, (err) => {
|
||||
```vue
|
||||
<!-- ComponentA.vue -->
|
||||
<script setup lang="ts">
|
||||
const { data } = await useFetch('/api/user', { key: 'current-user' })
|
||||
const { data } = await useFetch('/api/user', { key: 'current-user' });
|
||||
</script>
|
||||
|
||||
<!-- ComponentB.vue -->
|
||||
<script setup lang="ts">
|
||||
// Access cached data without refetching
|
||||
const { data: user } = useNuxtData('current-user')
|
||||
const { data: user } = useNuxtData('current-user');
|
||||
|
||||
// Or refresh it
|
||||
const { refresh } = await useFetch('/api/user', { key: 'current-user' })
|
||||
const { refresh } = await useFetch('/api/user', { key: 'current-user' });
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -334,7 +331,7 @@ const { refresh } = await useFetch('/api/user', { key: 'current-user' })
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Don't trigger Pinia actions or side effects
|
||||
await useAsyncData(() => store.fetchUser()) // Can cause issues
|
||||
await useAsyncData(() => store.fetchUser()); // Can cause issues
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -343,12 +340,12 @@ await useAsyncData(() => store.fetchUser()) // Can cause issues
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
await callOnce(async () => {
|
||||
await store.fetchUser()
|
||||
})
|
||||
await store.fetchUser();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/getting-started/data-fetching
|
||||
- https://nuxt.com/docs/api/composables/use-fetch
|
||||
|
||||
@@ -16,10 +16,10 @@ This error occurs when calling Nuxt composables outside the proper context.
|
||||
```ts
|
||||
// composables/bad.ts
|
||||
// Called at module level - no Nuxt context!
|
||||
const config = useRuntimeConfig()
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
export function useMyComposable() {
|
||||
return config.public.apiBase
|
||||
return config.public.apiBase;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -29,14 +29,15 @@ export function useMyComposable() {
|
||||
// composables/good.ts
|
||||
export function useMyComposable() {
|
||||
// Called inside the composable - has context
|
||||
const config = useRuntimeConfig()
|
||||
return config.public.apiBase
|
||||
const config = useRuntimeConfig();
|
||||
return config.public.apiBase;
|
||||
}
|
||||
```
|
||||
|
||||
### Valid Contexts for Composables
|
||||
|
||||
Nuxt composables work in:
|
||||
|
||||
- `<script setup>` blocks
|
||||
- `setup()` function
|
||||
- `defineNuxtPlugin()` callbacks
|
||||
@@ -45,13 +46,13 @@ Nuxt composables work in:
|
||||
```ts
|
||||
// ✅ Plugin
|
||||
export default defineNuxtPlugin(() => {
|
||||
const config = useRuntimeConfig() // Works
|
||||
})
|
||||
const config = useRuntimeConfig(); // Works
|
||||
});
|
||||
|
||||
// ✅ Middleware
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const route = useRoute() // Works
|
||||
})
|
||||
const route = useRoute(); // Works
|
||||
});
|
||||
```
|
||||
|
||||
## Avoid State Leaks Between Requests
|
||||
@@ -61,10 +62,10 @@ export default defineNuxtRouteMiddleware(() => {
|
||||
```ts
|
||||
// composables/bad.ts
|
||||
// This state is SHARED between all requests on server!
|
||||
const globalState = ref({ user: null })
|
||||
const globalState = ref({ user: null });
|
||||
|
||||
export function useUser() {
|
||||
return globalState
|
||||
return globalState;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -74,13 +75,14 @@ export function useUser() {
|
||||
// composables/good.ts
|
||||
export function useUser() {
|
||||
// useState creates request-isolated state
|
||||
return useState('user', () => ({ user: null }))
|
||||
return useState('user', () => ({ user: null }));
|
||||
}
|
||||
```
|
||||
|
||||
### Why This Matters
|
||||
|
||||
On the server, module-level state persists across requests, causing:
|
||||
|
||||
- Data leaking between users
|
||||
- Security vulnerabilities
|
||||
- Memory leaks
|
||||
@@ -94,7 +96,7 @@ Hydration mismatches occur when server HTML differs from client render.
|
||||
```vue
|
||||
<script setup>
|
||||
// localStorage doesn't exist on server!
|
||||
const theme = localStorage.getItem('theme') || 'light'
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -103,7 +105,7 @@ const theme = localStorage.getItem('theme') || 'light'
|
||||
```vue
|
||||
<script setup>
|
||||
// useCookie works on both server and client
|
||||
const theme = useCookie('theme', { default: () => 'light' })
|
||||
const theme = useCookie('theme', { default: () => 'light' });
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -121,7 +123,7 @@ const theme = useCookie('theme', { default: () => 'light' })
|
||||
```vue
|
||||
<script setup>
|
||||
// Value is generated once on server, hydrated on client
|
||||
const randomValue = useState('random', () => Math.random())
|
||||
const randomValue = useState('random', () => Math.random());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -162,7 +164,7 @@ const randomValue = useState('random', () => Math.random())
|
||||
<script setup>
|
||||
if (import.meta.client) {
|
||||
// Only runs in browser
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -186,9 +188,9 @@ onMounted(() => {
|
||||
```vue
|
||||
<script setup>
|
||||
onMounted(async () => {
|
||||
const { Chart } = await import('chart.js')
|
||||
new Chart(canvas.value, config)
|
||||
})
|
||||
const { Chart } = await import('chart.js');
|
||||
new Chart(canvas.value, config);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -200,7 +202,7 @@ onMounted(async () => {
|
||||
<script setup>
|
||||
if (import.meta.server) {
|
||||
// Only runs on server
|
||||
const secrets = useRuntimeConfig().apiSecret
|
||||
const secrets = useRuntimeConfig().apiSecret;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -211,7 +213,7 @@ if (import.meta.server) {
|
||||
<!-- components/ServerData.server.vue -->
|
||||
<script setup>
|
||||
// This entire component only runs on server
|
||||
const data = await fetchSensitiveData()
|
||||
const data = await fetchSensitiveData();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -225,8 +227,8 @@ const data = await fetchSensitiveData()
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
await someAsyncOperation()
|
||||
const route = useRoute() // May fail - context lost after await
|
||||
await someAsyncOperation();
|
||||
const route = useRoute(); // May fail - context lost after await
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -235,10 +237,10 @@ const route = useRoute() // May fail - context lost after await
|
||||
```vue
|
||||
<script setup>
|
||||
// Get all composables before any await
|
||||
const route = useRoute()
|
||||
const config = useRuntimeConfig()
|
||||
const route = useRoute();
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
await someAsyncOperation()
|
||||
await someAsyncOperation();
|
||||
// Now safe to use route and config
|
||||
</script>
|
||||
```
|
||||
@@ -251,8 +253,8 @@ await someAsyncOperation()
|
||||
// plugins/analytics.client.ts
|
||||
export default defineNuxtPlugin(() => {
|
||||
// Only runs on client
|
||||
initAnalytics()
|
||||
})
|
||||
initAnalytics();
|
||||
});
|
||||
```
|
||||
|
||||
### Server-only Plugins
|
||||
@@ -261,8 +263,8 @@ export default defineNuxtPlugin(() => {
|
||||
// plugins/server-init.server.ts
|
||||
export default defineNuxtPlugin(() => {
|
||||
// Only runs on server
|
||||
initServerConnections()
|
||||
})
|
||||
initServerConnections();
|
||||
});
|
||||
```
|
||||
|
||||
### Provide/Inject Pattern
|
||||
@@ -270,20 +272,20 @@ export default defineNuxtPlugin(() => {
|
||||
```ts
|
||||
// plugins/api.ts
|
||||
export default defineNuxtPlugin(() => {
|
||||
const api = createApiClient()
|
||||
const api = createApiClient();
|
||||
|
||||
return {
|
||||
provide: {
|
||||
api,
|
||||
},
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const { $api } = useNuxtApp()
|
||||
const data = await $api.get('/users')
|
||||
const { $api } = useNuxtApp();
|
||||
const data = await $api.get('/users');
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -293,7 +295,7 @@ const data = await $api.get('/users')
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import SomeLibrary from 'browser-only-lib' // Breaks SSR
|
||||
import SomeLibrary from 'browser-only-lib'; // Breaks SSR
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -329,8 +331,8 @@ onMounted(async () => {
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
console.log('Server:', import.meta.server)
|
||||
console.log('Client:', import.meta.client)
|
||||
console.log('Server:', import.meta.server);
|
||||
console.log('Client:', import.meta.client);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -340,14 +342,14 @@ DevTools shows payload data and hydration state.
|
||||
|
||||
### Common Error Messages
|
||||
|
||||
| Error | Cause |
|
||||
|-------|-------|
|
||||
| Error | Cause |
|
||||
| --------------------------- | --------------------------------------- |
|
||||
| "Nuxt instance unavailable" | Composable called outside setup context |
|
||||
| "Hydration mismatch" | Server/client HTML differs |
|
||||
| "window is not defined" | Browser API used during SSR |
|
||||
| "document is not defined" | DOM access during SSR |
|
||||
| "Hydration mismatch" | Server/client HTML differs |
|
||||
| "window is not defined" | Browser API used during SSR |
|
||||
| "document is not defined" | DOM access during SSR |
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables
|
||||
- https://nuxt.com/docs/guide/best-practices/hydration
|
||||
|
||||
@@ -76,6 +76,7 @@ npx nuxt dev --tunnel
|
||||
| `--clear` | Clear console on restart |
|
||||
|
||||
**Environment Variables:**
|
||||
|
||||
- `NUXT_PORT` or `PORT` - Default port
|
||||
- `NUXT_HOST` or `HOST` - Default host
|
||||
|
||||
@@ -254,7 +255,7 @@ npx nuxt dev --extends ./base-layer
|
||||
npx nuxt build --extends ./base-layer
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/api/commands/dev
|
||||
- https://nuxt.com/docs/api/commands/build
|
||||
|
||||
@@ -17,7 +17,7 @@ export default defineNuxtConfig({
|
||||
// Configuration options
|
||||
devtools: { enabled: true },
|
||||
modules: ['@nuxt/ui'],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Environment Overrides
|
||||
@@ -39,7 +39,7 @@ export default defineNuxtConfig({
|
||||
// Staging environment config
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Use `--envName` flag to select environment: `nuxt build --envName staging`
|
||||
@@ -59,7 +59,7 @@ export default defineNuxtConfig({
|
||||
apiBase: '/api',
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Override with environment variables:
|
||||
@@ -74,7 +74,7 @@ Access in components/composables:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const config = useRuntimeConfig()
|
||||
const config = useRuntimeConfig();
|
||||
// Server: config.apiSecret, config.public.apiBase
|
||||
// Client: config.public.apiBase only
|
||||
</script>
|
||||
@@ -94,26 +94,26 @@ export default defineAppConfig({
|
||||
primary: '#ff0000',
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Access in components:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const appConfig = useAppConfig()
|
||||
const appConfig = useAppConfig();
|
||||
</script>
|
||||
```
|
||||
|
||||
## runtimeConfig vs app.config
|
||||
|
||||
| Feature | runtimeConfig | app.config |
|
||||
|---------|--------------|------------|
|
||||
| Client-side | Hydrated | Bundled |
|
||||
| Environment variables | Yes | No |
|
||||
| Reactive | Yes | Yes |
|
||||
| Hot module replacement | No | Yes |
|
||||
| Non-primitive JS types | No | Yes |
|
||||
| Feature | runtimeConfig | app.config |
|
||||
| ---------------------- | ------------- | ---------- |
|
||||
| Client-side | Hydrated | Bundled |
|
||||
| Environment variables | Yes | No |
|
||||
| Reactive | Yes | Yes |
|
||||
| Hot module replacement | No | Yes |
|
||||
| Non-primitive JS types | No | Yes |
|
||||
|
||||
**Use runtimeConfig** for secrets and values that change per environment.
|
||||
**Use app.config** for public tokens, theme settings, and non-sensitive config.
|
||||
@@ -139,7 +139,7 @@ export default defineNuxtConfig({
|
||||
postcss: {
|
||||
// postcss options
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Vue Configuration
|
||||
@@ -151,10 +151,10 @@ export default defineNuxtConfig({
|
||||
vue: {
|
||||
propsDestructure: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/getting-started/configuration
|
||||
- https://nuxt.com/docs/guide/going-further/runtime-config
|
||||
|
||||
@@ -19,7 +19,7 @@ Primary composable for fetching data in components:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data, status, error, refresh, clear } = await useFetch('/api/posts')
|
||||
const { data, status, error, refresh, clear } = await useFetch('/api/posts');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -46,7 +46,7 @@ const { data } = await useFetch('/api/posts', {
|
||||
// Only pick specific fields
|
||||
pick: ['id', 'title'],
|
||||
// Transform response
|
||||
transform: (posts) => posts.map(p => ({ ...p, slug: slugify(p.title) })),
|
||||
transform: (posts) => posts.map((p) => ({ ...p, slug: slugify(p.title) })),
|
||||
// Custom key for caching
|
||||
key: 'posts-list',
|
||||
// Don't fetch on server
|
||||
@@ -57,17 +57,17 @@ const { data } = await useFetch('/api/posts', {
|
||||
immediate: false,
|
||||
// Default value
|
||||
default: () => [],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Reactive Parameters
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const page = ref(1)
|
||||
const page = ref(1);
|
||||
const { data } = await useFetch('/api/posts', {
|
||||
query: { page }, // Automatically refetches when page changes
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -75,8 +75,8 @@ const { data } = await useFetch('/api/posts', {
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const id = ref(1)
|
||||
const { data } = await useFetch(() => `/api/posts/${id.value}`)
|
||||
const id = ref(1);
|
||||
const { data } = await useFetch(() => `/api/posts/${id.value}`);
|
||||
// Refetches when id changes
|
||||
</script>
|
||||
```
|
||||
@@ -88,8 +88,8 @@ For wrapping any async function:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data, error } = await useAsyncData('user', () => {
|
||||
return myCustomFetch('/user/profile')
|
||||
})
|
||||
return myCustomFetch('/user/profile');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -98,12 +98,9 @@ const { data, error } = await useAsyncData('user', () => {
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data } = await useAsyncData('cart', async () => {
|
||||
const [coupons, offers] = await Promise.all([
|
||||
$fetch('/api/coupons'),
|
||||
$fetch('/api/offers'),
|
||||
])
|
||||
return { coupons, offers }
|
||||
})
|
||||
const [coupons, offers] = await Promise.all([$fetch('/api/coupons'), $fetch('/api/offers')]);
|
||||
return { coupons, offers };
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -117,7 +114,7 @@ async function submitForm() {
|
||||
const result = await $fetch('/api/submit', {
|
||||
method: 'POST',
|
||||
body: { name: 'John' },
|
||||
})
|
||||
});
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -128,14 +125,14 @@ async function submitForm() {
|
||||
|
||||
All composables return:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `data` | `Ref<T>` | Fetched data |
|
||||
| `error` | `Ref<Error>` | Error if request failed |
|
||||
| `status` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | Request status |
|
||||
| `refresh` | `() => Promise` | Refetch data |
|
||||
| `execute` | `() => Promise` | Alias for refresh |
|
||||
| `clear` | `() => void` | Reset data and error |
|
||||
| Property | Type | Description |
|
||||
| --------- | -------------------------------------------------- | ----------------------- |
|
||||
| `data` | `Ref<T>` | Fetched data |
|
||||
| `error` | `Ref<Error>` | Error if request failed |
|
||||
| `status` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | Request status |
|
||||
| `refresh` | `() => Promise` | Refetch data |
|
||||
| `execute` | `() => Promise` | Alias for refresh |
|
||||
| `clear` | `() => void` | Reset data and error |
|
||||
|
||||
## Lazy Fetching
|
||||
|
||||
@@ -144,11 +141,11 @@ Don't block navigation:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Using lazy option
|
||||
const { data, status } = await useFetch('/api/posts', { lazy: true })
|
||||
const { data, status } = await useFetch('/api/posts', { lazy: true });
|
||||
|
||||
// Or use lazy variants
|
||||
const { data, status } = await useLazyFetch('/api/posts')
|
||||
const { data, status } = await useLazyAsyncData('key', fetchFn)
|
||||
const { data, status } = await useLazyFetch('/api/posts');
|
||||
const { data, status } = await useLazyAsyncData('key', fetchFn);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -156,16 +153,16 @@ const { data, status } = await useLazyAsyncData('key', fetchFn)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const category = ref('tech')
|
||||
const category = ref('tech');
|
||||
|
||||
const { data, refresh } = await useFetch('/api/posts', {
|
||||
query: { category },
|
||||
// Auto-refresh when category changes
|
||||
watch: [category],
|
||||
})
|
||||
});
|
||||
|
||||
// Manual refresh
|
||||
const refreshData = () => refresh()
|
||||
const refreshData = () => refresh();
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -176,10 +173,10 @@ Data is cached by key. Share data across components:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// In component A
|
||||
const { data } = await useFetch('/api/user', { key: 'current-user' })
|
||||
const { data } = await useFetch('/api/user', { key: 'current-user' });
|
||||
|
||||
// In component B - uses cached data
|
||||
const { data } = useNuxtData('current-user')
|
||||
const { data } = useNuxtData('current-user');
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -187,13 +184,13 @@ Refresh cached data globally:
|
||||
|
||||
```ts
|
||||
// Refresh specific key
|
||||
await refreshNuxtData('current-user')
|
||||
await refreshNuxtData('current-user');
|
||||
|
||||
// Refresh all data
|
||||
await refreshNuxtData()
|
||||
await refreshNuxtData();
|
||||
|
||||
// Clear cached data
|
||||
clearNuxtData('current-user')
|
||||
clearNuxtData('current-user');
|
||||
```
|
||||
|
||||
## Interceptors
|
||||
@@ -201,20 +198,20 @@ clearNuxtData('current-user')
|
||||
```ts
|
||||
const { data } = await useFetch('/api/auth', {
|
||||
onRequest({ options }) {
|
||||
options.headers.set('Authorization', `Bearer ${token}`)
|
||||
options.headers.set('Authorization', `Bearer ${token}`);
|
||||
},
|
||||
onRequestError({ error }) {
|
||||
console.error('Request failed:', error)
|
||||
console.error('Request failed:', error);
|
||||
},
|
||||
onResponse({ response }) {
|
||||
// Process response
|
||||
},
|
||||
onResponseError({ response }) {
|
||||
if (response.status === 401) {
|
||||
navigateTo('/login')
|
||||
navigateTo('/login');
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Passing Headers (SSR)
|
||||
@@ -223,12 +220,12 @@ const { data } = await useFetch('/api/auth', {
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const headers = useRequestHeaders(['cookie'])
|
||||
const data = await $fetch('/api/user', { headers })
|
||||
const headers = useRequestHeaders(['cookie']);
|
||||
const data = await $fetch('/api/user', { headers });
|
||||
</script>
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/getting-started/data-fetching
|
||||
- https://nuxt.com/docs/api/composables/use-fetch
|
||||
|
||||
@@ -22,6 +22,7 @@ node .output/server/index.mjs
|
||||
```
|
||||
|
||||
Environment variables:
|
||||
|
||||
- `PORT` or `NITRO_PORT` (default: 3000)
|
||||
- `HOST` or `NITRO_HOST` (default: 0.0.0.0)
|
||||
|
||||
@@ -42,7 +43,7 @@ export default defineNuxtConfig({
|
||||
nitro: {
|
||||
preset: 'vercel', // or 'netlify', 'cloudflare-pages', etc.
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Or via environment variable:
|
||||
@@ -70,6 +71,7 @@ vercel
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Zero configuration for Nuxt (auto-detects)
|
||||
- Excellent preview deployments for PRs
|
||||
- Built-in analytics and speed insights
|
||||
@@ -77,6 +79,7 @@ vercel
|
||||
- Great free tier for personal projects
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Can get expensive at scale (bandwidth costs)
|
||||
- Vendor lock-in concerns
|
||||
- Limited build minutes on free tier
|
||||
@@ -98,6 +101,7 @@ netlify deploy --prod
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Great free tier with generous bandwidth
|
||||
- Built-in forms, identity, and functions
|
||||
- Excellent for static sites with some dynamic features
|
||||
@@ -105,6 +109,7 @@ netlify deploy --prod
|
||||
- Split testing built-in
|
||||
|
||||
**Cons:**
|
||||
|
||||
- SSR/serverless functions can be slower than Vercel
|
||||
- Less optimized for full SSR apps
|
||||
- Build minutes can run out on free tier
|
||||
@@ -123,6 +128,7 @@ NITRO_PRESET=cloudflare-pages nuxt build
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Unlimited bandwidth on free tier
|
||||
- Excellent global edge network (fastest TTFB)
|
||||
- Workers for edge computing
|
||||
@@ -130,6 +136,7 @@ NITRO_PRESET=cloudflare-pages nuxt build
|
||||
- D1, KV, R2 for data storage
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Workers have execution limits (CPU time)
|
||||
- Some Node.js APIs not available in Workers
|
||||
- Less mature than Vercel/Netlify for frameworks
|
||||
@@ -157,16 +164,17 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
|
||||
# Deploy to your server (example: rsync to VPS)
|
||||
- name: Deploy to server
|
||||
run: rsync -avz .output/ user@server:/app/
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Full control over build and deployment
|
||||
- No vendor lock-in
|
||||
- Can deploy anywhere (VPS, Docker, Kubernetes)
|
||||
@@ -174,6 +182,7 @@ jobs:
|
||||
- Customizable workflows
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Requires more setup and maintenance
|
||||
- Need to manage your own infrastructure
|
||||
- No built-in preview deployments
|
||||
@@ -185,14 +194,14 @@ jobs:
|
||||
|
||||
## Quick Decision Guide
|
||||
|
||||
| Need | Recommendation |
|
||||
|------|----------------|
|
||||
| Fastest setup, small team | **Vercel** |
|
||||
| Static site with forms | **Netlify** |
|
||||
| Cost-sensitive at scale | **Cloudflare Pages** |
|
||||
| Full control / enterprise | **GitHub Actions + VPS** |
|
||||
| Docker/Kubernetes | **GitHub Actions + Container Registry** |
|
||||
| Serverless APIs | **Vercel** or **AWS Lambda** |
|
||||
| Need | Recommendation |
|
||||
| ------------------------- | --------------------------------------- |
|
||||
| Fastest setup, small team | **Vercel** |
|
||||
| Static site with forms | **Netlify** |
|
||||
| Cost-sensitive at scale | **Cloudflare Pages** |
|
||||
| Full control / enterprise | **GitHub Actions + VPS** |
|
||||
| Docker/Kubernetes | **GitHub Actions + Container Registry** |
|
||||
| Serverless APIs | **Vercel** or **AWS Lambda** |
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
@@ -217,7 +226,7 @@ docker build -t my-nuxt-app .
|
||||
docker run -p 3000:3000 my-nuxt-app
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/getting-started/deployment
|
||||
- https://nitro.build/deploy
|
||||
|
||||
@@ -48,7 +48,7 @@ Contains all application code. Can also be at root level (without `app/` folder)
|
||||
// nuxt.config.ts - customize source directory
|
||||
export default defineNuxtConfig({
|
||||
srcDir: 'src/', // Change from 'app/' to 'src/'
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### `app/components/`
|
||||
@@ -99,7 +99,7 @@ Re-export nested composables:
|
||||
|
||||
```ts
|
||||
// composables/index.ts
|
||||
export { useHelper } from './nested/utils'
|
||||
export { useHelper } from './nested/utils';
|
||||
```
|
||||
|
||||
### `app/pages/`
|
||||
@@ -153,7 +153,7 @@ Use in pages:
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
// layout: false // Disable layout
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -240,28 +240,28 @@ Reference in components:
|
||||
|
||||
## Special Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app.vue` | Root component (optional with pages/) |
|
||||
| `app.config.ts` | Runtime app configuration |
|
||||
| `error.vue` | Custom error page |
|
||||
| `nuxt.config.ts` | Build-time configuration |
|
||||
| `.nuxtignore` | Ignore files from Nuxt |
|
||||
| `.env` | Environment variables |
|
||||
| File | Purpose |
|
||||
| ---------------- | ------------------------------------- |
|
||||
| `app.vue` | Root component (optional with pages/) |
|
||||
| `app.config.ts` | Runtime app configuration |
|
||||
| `error.vue` | Custom error page |
|
||||
| `nuxt.config.ts` | Build-time configuration |
|
||||
| `.nuxtignore` | Ignore files from Nuxt |
|
||||
| `.env` | Environment variables |
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
| Pattern | Meaning |
|
||||
|---------|---------|
|
||||
| `[param]` | Dynamic route parameter |
|
||||
| `[[param]]` | Optional parameter |
|
||||
| `[...slug]` | Catch-all route |
|
||||
| `(group)` | Route group (not in URL) |
|
||||
| `.client.vue` | Client-only component |
|
||||
| `.server.vue` | Server-only component |
|
||||
| `.global.ts` | Global middleware |
|
||||
| Pattern | Meaning |
|
||||
| ------------- | ------------------------ |
|
||||
| `[param]` | Dynamic route parameter |
|
||||
| `[[param]]` | Optional parameter |
|
||||
| `[...slug]` | Catch-all route |
|
||||
| `(group)` | Route group (not in URL) |
|
||||
| `.client.vue` | Client-only component |
|
||||
| `.server.vue` | Server-only component |
|
||||
| `.global.ts` | Global middleware |
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/directory-structure
|
||||
- https://nuxt.com/docs/directory-structure/app
|
||||
|
||||
@@ -21,12 +21,12 @@ export default defineNuxtConfig({
|
||||
'./modules/my-module',
|
||||
// Inline module
|
||||
(options, nuxt) => {
|
||||
console.log('Inline module')
|
||||
console.log('Inline module');
|
||||
},
|
||||
// With options
|
||||
['@nuxt/image', { provider: 'cloudinary' }],
|
||||
],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Creating Modules
|
||||
@@ -44,76 +44,76 @@ export default defineNuxtModule({
|
||||
enabled: true,
|
||||
},
|
||||
setup(options, nuxt) {
|
||||
if (!options.enabled) return
|
||||
if (!options.enabled) return;
|
||||
|
||||
console.log('My module is running!')
|
||||
console.log('My module is running!');
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Adding Components
|
||||
|
||||
```ts
|
||||
// modules/ui/index.ts
|
||||
import { addComponent, createResolver } from '@nuxt/kit'
|
||||
import { addComponent, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup(options, nuxt) {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Add single component
|
||||
addComponent({
|
||||
name: 'MyButton',
|
||||
filePath: resolve('./runtime/components/MyButton.vue'),
|
||||
})
|
||||
});
|
||||
|
||||
// Add components directory
|
||||
addComponentsDir({
|
||||
path: resolve('./runtime/components'),
|
||||
prefix: 'My',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Adding Composables
|
||||
|
||||
```ts
|
||||
// modules/utils/index.ts
|
||||
import { addImports, createResolver } from '@nuxt/kit'
|
||||
import { addImports, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
// Add auto-imported composable
|
||||
addImports({
|
||||
name: 'useMyUtil',
|
||||
from: resolve('./runtime/composables/useMyUtil'),
|
||||
})
|
||||
});
|
||||
|
||||
// Add directory for auto-imports
|
||||
addImportsDir(resolve('./runtime/composables'))
|
||||
addImportsDir(resolve('./runtime/composables'));
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Adding Plugins
|
||||
|
||||
```ts
|
||||
// modules/analytics/index.ts
|
||||
import { addPlugin, createResolver } from '@nuxt/kit'
|
||||
import { addPlugin, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
addPlugin({
|
||||
src: resolve('./runtime/plugin'),
|
||||
mode: 'client', // 'client', 'server', or 'all'
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Plugin file:
|
||||
@@ -122,27 +122,27 @@ Plugin file:
|
||||
// modules/analytics/runtime/plugin.ts
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
console.log('Page loaded')
|
||||
})
|
||||
})
|
||||
console.log('Page loaded');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Adding Server Routes
|
||||
|
||||
```ts
|
||||
// modules/api/index.ts
|
||||
import { addServerHandler, createResolver } from '@nuxt/kit'
|
||||
import { addServerHandler, createResolver } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
const { resolve } = createResolver(import.meta.url);
|
||||
|
||||
addServerHandler({
|
||||
route: '/api/my-endpoint',
|
||||
handler: resolve('./runtime/server/api/my-endpoint'),
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Extending Config
|
||||
@@ -152,19 +152,19 @@ export default defineNuxtModule({
|
||||
export default defineNuxtModule({
|
||||
setup(options, nuxt) {
|
||||
// Add CSS
|
||||
nuxt.options.css.push('my-module/styles.css')
|
||||
nuxt.options.css.push('my-module/styles.css');
|
||||
|
||||
// Add runtime config
|
||||
nuxt.options.runtimeConfig.public.myModule = {
|
||||
apiUrl: options.apiUrl,
|
||||
}
|
||||
};
|
||||
|
||||
// Extend Vite config
|
||||
nuxt.options.vite.optimizeDeps ||= {}
|
||||
nuxt.options.vite.optimizeDeps.include ||= []
|
||||
nuxt.options.vite.optimizeDeps.include.push('some-package')
|
||||
nuxt.options.vite.optimizeDeps ||= {};
|
||||
nuxt.options.vite.optimizeDeps.include ||= [];
|
||||
nuxt.options.vite.optimizeDeps.include.push('some-package');
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Module Hooks
|
||||
@@ -174,26 +174,26 @@ export default defineNuxtModule({
|
||||
setup(options, nuxt) {
|
||||
// Build-time hooks
|
||||
nuxt.hook('modules:done', () => {
|
||||
console.log('All modules loaded')
|
||||
})
|
||||
console.log('All modules loaded');
|
||||
});
|
||||
|
||||
nuxt.hook('components:dirs', (dirs) => {
|
||||
dirs.push({ path: '~/extra-components' })
|
||||
})
|
||||
dirs.push({ path: '~/extra-components' });
|
||||
});
|
||||
|
||||
nuxt.hook('pages:extend', (pages) => {
|
||||
pages.push({
|
||||
name: 'custom-page',
|
||||
path: '/custom',
|
||||
file: resolve('./runtime/pages/custom.vue'),
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
nuxt.hook('imports:extend', (imports) => {
|
||||
imports.push({ name: 'myHelper', from: 'my-package' })
|
||||
})
|
||||
imports.push({ name: 'myHelper', from: 'my-package' });
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Module Options
|
||||
@@ -202,9 +202,9 @@ Type-safe options with defaults:
|
||||
|
||||
```ts
|
||||
export interface ModuleOptions {
|
||||
apiKey: string
|
||||
enabled?: boolean
|
||||
prefix?: string
|
||||
apiKey: string;
|
||||
enabled?: boolean;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
@@ -219,10 +219,10 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
setup(options, nuxt) {
|
||||
// options is typed as ModuleOptions
|
||||
if (!options.apiKey) {
|
||||
console.warn('API key not provided')
|
||||
console.warn('API key not provided');
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Usage:
|
||||
@@ -235,7 +235,7 @@ export default defineNuxtConfig({
|
||||
apiKey: 'xxx',
|
||||
prefix: 'Custom',
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Local Modules
|
||||
@@ -260,7 +260,7 @@ export default defineNuxtConfig({
|
||||
modules: [
|
||||
'~/modules/my-module', // Explicit
|
||||
],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Module Dependencies
|
||||
@@ -281,10 +281,10 @@ export default defineNuxtModule({
|
||||
setup() {
|
||||
// @nuxt/image is guaranteed to be installed
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/guide/modules
|
||||
- https://nuxt.com/docs/guide/modules/module-anatomy
|
||||
|
||||
@@ -35,9 +35,9 @@ Access route parameters:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const route = useRoute();
|
||||
// /posts/123 → route.params.id = '123'
|
||||
console.log(route.params.id)
|
||||
console.log(route.params.id);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -61,12 +61,12 @@ NuxtLink automatically prefetches linked pages when they enter the viewport.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
function goToPost(id: number) {
|
||||
navigateTo(`/posts/${id}`)
|
||||
navigateTo(`/posts/${id}`);
|
||||
// or
|
||||
router.push({ name: 'posts-id', params: { id } })
|
||||
router.push({ name: 'posts-id', params: { id } });
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -78,12 +78,12 @@ function goToPost(id: number) {
|
||||
```ts
|
||||
// middleware/auth.ts
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const isAuthenticated = false // Your auth logic
|
||||
const isAuthenticated = false; // Your auth logic
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return navigateTo('/login')
|
||||
return navigateTo('/login');
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Apply to pages:
|
||||
@@ -93,7 +93,7 @@ Apply to pages:
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
// or multiple: middleware: ['auth', 'admin']
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -104,8 +104,8 @@ Name files with `.global` suffix:
|
||||
```ts
|
||||
// middleware/logging.global.ts
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
console.log('Navigating to:', to.path)
|
||||
})
|
||||
console.log('Navigating to:', to.path);
|
||||
});
|
||||
```
|
||||
|
||||
### Inline Middleware
|
||||
@@ -118,7 +118,7 @@ definePageMeta({
|
||||
// Inline middleware logic
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -134,9 +134,9 @@ definePageMeta({
|
||||
middleware: 'auth',
|
||||
validate: (route) => {
|
||||
// Return false for 404, or object with status/statusText
|
||||
return /^\d+$/.test(route.params.id as string)
|
||||
return /^\d+$/.test(route.params.id as string);
|
||||
},
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -147,9 +147,9 @@ definePageMeta({
|
||||
definePageMeta({
|
||||
validate: (route) => {
|
||||
// Must return boolean or object with status
|
||||
return typeof route.params.id === 'string' && /^\d+$/.test(route.params.id)
|
||||
return typeof route.params.id === 'string' && /^\d+$/.test(route.params.id);
|
||||
},
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -186,7 +186,7 @@ Use in pages:
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -194,10 +194,10 @@ Dynamic layout:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const layout = ref('default')
|
||||
const layout = ref('default');
|
||||
|
||||
function enableAdmin() {
|
||||
setPageLayout('admin')
|
||||
setPageLayout('admin');
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -208,17 +208,17 @@ function enableAdmin() {
|
||||
<script setup lang="ts">
|
||||
onBeforeRouteLeave((to, from) => {
|
||||
// Confirm before leaving
|
||||
const answer = window.confirm('Leave?')
|
||||
if (!answer) return false
|
||||
})
|
||||
const answer = window.confirm('Leave?');
|
||||
if (!answer) return false;
|
||||
});
|
||||
|
||||
onBeforeRouteUpdate((to, from) => {
|
||||
// Called when route changes but component is reused
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/getting-started/routing
|
||||
- https://nuxt.com/docs/directory-structure/app/pages
|
||||
|
||||
+18
-16
@@ -55,10 +55,11 @@ export default defineNuxtConfig({
|
||||
pathPrefix: false, // Use filename only
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
With `pathPrefix: false`:
|
||||
|
||||
```
|
||||
components/base/Button.vue → <Button />
|
||||
```
|
||||
@@ -69,7 +70,7 @@ Prefix with `Lazy` for dynamic imports:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const showChart = ref(false)
|
||||
const showChart = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -80,6 +81,7 @@ const showChart = ref(false)
|
||||
```
|
||||
|
||||
Benefits:
|
||||
|
||||
- Reduces initial bundle size
|
||||
- Code-splits component into separate chunk
|
||||
- Loads on-demand
|
||||
@@ -148,7 +150,7 @@ Hydrate on condition:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const isReady = ref(false)
|
||||
const isReady = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -175,7 +177,7 @@ Never hydrate (static only):
|
||||
|
||||
<script setup>
|
||||
function onChartReady() {
|
||||
console.log('Chart is now interactive')
|
||||
console.log('Chart is now interactive');
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -218,7 +220,7 @@ export default defineNuxtConfig({
|
||||
experimental: {
|
||||
componentIslands: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Paired Components
|
||||
@@ -235,9 +237,9 @@ Server version renders during SSR, client version takes over after hydration.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { SomeComponent } from '#components'
|
||||
import { SomeComponent } from '#components';
|
||||
|
||||
const dynamicComponent = resolveComponent('MyButton')
|
||||
const dynamicComponent = resolveComponent('MyButton');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -252,7 +254,7 @@ Bypass auto-imports when needed:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { LazyMountainsList, NuxtLink } from '#components'
|
||||
import { LazyMountainsList, NuxtLink } from '#components';
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -266,7 +268,7 @@ export default defineNuxtConfig({
|
||||
{ path: '~/components/forms', prefix: 'Form' },
|
||||
'~/components', // Default, should come last
|
||||
],
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Global Components
|
||||
@@ -280,7 +282,7 @@ export default defineNuxtConfig({
|
||||
global: true,
|
||||
dirs: ['~/components'],
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Or use `.global.vue` suffix:
|
||||
@@ -298,7 +300,7 @@ export default defineNuxtConfig({
|
||||
components: {
|
||||
dirs: [], // Disable auto-imports
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Library Authors
|
||||
@@ -307,21 +309,21 @@ Register components from npm package:
|
||||
|
||||
```ts
|
||||
// my-ui-lib/nuxt.ts
|
||||
import { addComponentsDir, createResolver, defineNuxtModule } from '@nuxt/kit'
|
||||
import { addComponentsDir, createResolver, defineNuxtModule } from '@nuxt/kit';
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
const resolver = createResolver(import.meta.url)
|
||||
const resolver = createResolver(import.meta.url);
|
||||
|
||||
addComponentsDir({
|
||||
path: resolver.resolve('./components'),
|
||||
prefix: 'MyUi',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/directory-structure/app/components
|
||||
- https://nuxt.com/docs/guide/concepts/auto-imports#auto-imported-components
|
||||
|
||||
@@ -29,11 +29,7 @@ Optimized link component with prefetching:
|
||||
<NuxtLink to="/page" replace>Replace</NuxtLink>
|
||||
|
||||
<!-- Custom active class -->
|
||||
<NuxtLink
|
||||
to="/dashboard"
|
||||
active-class="text-primary"
|
||||
exact-active-class="font-bold"
|
||||
>
|
||||
<NuxtLink to="/dashboard" active-class="text-primary" exact-active-class="font-bold">
|
||||
Dashboard
|
||||
</NuxtLink>
|
||||
</template>
|
||||
@@ -91,7 +87,7 @@ Dynamic layout:
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const layout = computed(() => isAdmin ? 'admin' : 'default')
|
||||
const layout = computed(() => (isAdmin ? 'admin' : 'default'));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -144,7 +140,7 @@ Catch and handle errors in child components:
|
||||
|
||||
<script setup>
|
||||
function handleError(error) {
|
||||
console.error('Error caught:', error)
|
||||
console.error('Error caught:', error);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -205,10 +201,7 @@ Optimized images (requires `@nuxt/image` module):
|
||||
/>
|
||||
|
||||
<!-- Art direction with picture -->
|
||||
<NuxtPicture
|
||||
src="/images/hero.jpg"
|
||||
:img-attrs="{ alt: 'Hero image' }"
|
||||
/>
|
||||
<NuxtPicture src="/images/hero.jpg" :img-attrs="{ alt: 'Hero image' }" />
|
||||
</template>
|
||||
```
|
||||
|
||||
@@ -255,7 +248,7 @@ Accessibility: announces page changes to screen readers:
|
||||
</template>
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/api/components/nuxt-link
|
||||
- https://nuxt.com/docs/api/components/nuxt-page
|
||||
|
||||
@@ -14,25 +14,25 @@ Nuxt automatically imports Vue APIs, Nuxt composables, and your custom composabl
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// No imports needed - all auto-imported
|
||||
const count = ref(0)
|
||||
const doubled = computed(() => count.value * 2)
|
||||
const count = ref(0);
|
||||
const doubled = computed(() => count.value * 2);
|
||||
|
||||
watch(count, (newVal) => {
|
||||
console.log('Count changed:', newVal)
|
||||
})
|
||||
console.log('Count changed:', newVal);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Component mounted')
|
||||
})
|
||||
console.log('Component mounted');
|
||||
});
|
||||
|
||||
// Lifecycle hooks
|
||||
onBeforeMount(() => {})
|
||||
onUnmounted(() => {})
|
||||
onBeforeUnmount(() => {})
|
||||
onBeforeMount(() => {});
|
||||
onUnmounted(() => {});
|
||||
onBeforeUnmount(() => {});
|
||||
|
||||
// Reactivity
|
||||
const state = reactive({ name: 'John' })
|
||||
const shallow = shallowRef({ deep: 'object' })
|
||||
const state = reactive({ name: 'John' });
|
||||
const shallow = shallowRef({ deep: 'object' });
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -41,28 +41,28 @@ const shallow = shallowRef({ deep: 'object' })
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// All auto-imported
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const config = useRuntimeConfig()
|
||||
const appConfig = useAppConfig()
|
||||
const nuxtApp = useNuxtApp()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const config = useRuntimeConfig();
|
||||
const appConfig = useAppConfig();
|
||||
const nuxtApp = useNuxtApp();
|
||||
|
||||
// Data fetching
|
||||
const { data } = await useFetch('/api/data')
|
||||
const { data: asyncData } = await useAsyncData('key', () => fetchData())
|
||||
const { data } = await useFetch('/api/data');
|
||||
const { data: asyncData } = await useAsyncData('key', () => fetchData());
|
||||
|
||||
// State
|
||||
const state = useState('key', () => 'initial')
|
||||
const cookie = useCookie('token')
|
||||
const state = useState('key', () => 'initial');
|
||||
const cookie = useCookie('token');
|
||||
|
||||
// Head/SEO
|
||||
useHead({ title: 'My Page' })
|
||||
useSeoMeta({ description: 'Page description' })
|
||||
useHead({ title: 'My Page' });
|
||||
useSeoMeta({ description: 'Page description' });
|
||||
|
||||
// Request helpers (SSR)
|
||||
const headers = useRequestHeaders()
|
||||
const event = useRequestEvent()
|
||||
const url = useRequestURL()
|
||||
const headers = useRequestHeaders();
|
||||
const event = useRequestEvent();
|
||||
const url = useRequestURL();
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -73,32 +73,32 @@ const url = useRequestURL()
|
||||
```ts
|
||||
// composables/useCounter.ts
|
||||
export function useCounter(initial = 0) {
|
||||
const count = ref(initial)
|
||||
const increment = () => count.value++
|
||||
const decrement = () => count.value--
|
||||
return { count, increment, decrement }
|
||||
const count = ref(initial);
|
||||
const increment = () => count.value++;
|
||||
const decrement = () => count.value--;
|
||||
return { count, increment, decrement };
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// composables/useAuth.ts
|
||||
export function useAuth() {
|
||||
const user = useState<User | null>('user', () => null)
|
||||
const isLoggedIn = computed(() => !!user.value)
|
||||
const user = useState<User | null>('user', () => null);
|
||||
const isLoggedIn = computed(() => !!user.value);
|
||||
|
||||
async function login(credentials: Credentials) {
|
||||
user.value = await $fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: credentials,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await $fetch('/api/auth/logout', { method: 'POST' })
|
||||
user.value = null
|
||||
await $fetch('/api/auth/logout', { method: 'POST' });
|
||||
user.value = null;
|
||||
}
|
||||
|
||||
return { user, isLoggedIn, login, logout }
|
||||
return { user, isLoggedIn, login, logout };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -107,8 +107,8 @@ export function useAuth() {
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Auto-imported - no import statement needed
|
||||
const { count, increment } = useCounter(10)
|
||||
const { user, isLoggedIn, login } = useAuth()
|
||||
const { count, increment } = useCounter(10);
|
||||
const { user, isLoggedIn, login } = useAuth();
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -129,7 +129,7 @@ Re-export nested composables:
|
||||
|
||||
```ts
|
||||
// composables/index.ts
|
||||
export { useHelper } from './nested/helper'
|
||||
export { useHelper } from './nested/helper';
|
||||
```
|
||||
|
||||
Or configure scanning:
|
||||
@@ -143,7 +143,7 @@ export default defineNuxtConfig({
|
||||
'composables/**', // Scan all nested
|
||||
],
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Utilities (`app/utils/`)
|
||||
@@ -151,22 +151,22 @@ export default defineNuxtConfig({
|
||||
```ts
|
||||
// utils/format.ts
|
||||
export function formatDate(date: Date) {
|
||||
return date.toLocaleDateString()
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(amount)
|
||||
}).format(amount);
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Auto-imported
|
||||
const date = formatDate(new Date())
|
||||
const price = formatCurrency(99.99)
|
||||
const date = formatDate(new Date());
|
||||
const price = formatCurrency(99.99);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -175,21 +175,21 @@ const price = formatCurrency(99.99)
|
||||
```ts
|
||||
// server/utils/db.ts
|
||||
export function useDb() {
|
||||
return createDbConnection()
|
||||
return createDbConnection();
|
||||
}
|
||||
|
||||
// server/utils/auth.ts
|
||||
export function verifyToken(token: string) {
|
||||
return jwt.verify(token, process.env.JWT_SECRET)
|
||||
return jwt.verify(token, process.env.JWT_SECRET);
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// server/api/users.ts
|
||||
export default defineEventHandler(() => {
|
||||
const db = useDb() // Auto-imported
|
||||
return db.query('SELECT * FROM users')
|
||||
})
|
||||
const db = useDb(); // Auto-imported
|
||||
return db.query('SELECT * FROM users');
|
||||
});
|
||||
```
|
||||
|
||||
## Third-party Package Imports
|
||||
@@ -213,7 +213,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Explicit Imports
|
||||
@@ -222,7 +222,7 @@ Use `#imports` alias when needed:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, useFetch } from '#imports'
|
||||
import { ref, computed, useFetch } from '#imports';
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -232,7 +232,7 @@ Nuxt composables must be called in valid context:
|
||||
|
||||
```ts
|
||||
// ❌ Wrong - module level
|
||||
const config = useRuntimeConfig()
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
export function useMyComposable() {}
|
||||
```
|
||||
@@ -240,12 +240,13 @@ export function useMyComposable() {}
|
||||
```ts
|
||||
// ✅ Correct - inside function
|
||||
export function useMyComposable() {
|
||||
const config = useRuntimeConfig()
|
||||
return { apiBase: config.public.apiBase }
|
||||
const config = useRuntimeConfig();
|
||||
return { apiBase: config.public.apiBase };
|
||||
}
|
||||
```
|
||||
|
||||
**Valid contexts:**
|
||||
|
||||
- `<script setup>` block
|
||||
- `setup()` function
|
||||
- `defineNuxtPlugin()` callback
|
||||
@@ -265,10 +266,10 @@ export default defineNuxtConfig({
|
||||
imports: {
|
||||
scan: false,
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/guide/concepts/auto-imports
|
||||
- https://nuxt.com/docs/directory-structure/app/composables
|
||||
|
||||
@@ -14,8 +14,8 @@ Create files in `server/api/` directory:
|
||||
```ts
|
||||
// server/api/hello.ts
|
||||
export default defineEventHandler((event) => {
|
||||
return { message: 'Hello World' }
|
||||
})
|
||||
return { message: 'Hello World' };
|
||||
});
|
||||
```
|
||||
|
||||
Access at `/api/hello`.
|
||||
@@ -25,27 +25,27 @@ Access at `/api/hello`.
|
||||
```ts
|
||||
// server/api/users.get.ts - GET /api/users
|
||||
export default defineEventHandler(() => {
|
||||
return getUsers()
|
||||
})
|
||||
return getUsers();
|
||||
});
|
||||
|
||||
// server/api/users.post.ts - POST /api/users
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
return createUser(body)
|
||||
})
|
||||
const body = await readBody(event);
|
||||
return createUser(body);
|
||||
});
|
||||
|
||||
// server/api/users/[id].put.ts - PUT /api/users/:id
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, 'id')
|
||||
const body = await readBody(event)
|
||||
return updateUser(id, body)
|
||||
})
|
||||
const id = getRouterParam(event, 'id');
|
||||
const body = await readBody(event);
|
||||
return updateUser(id, body);
|
||||
});
|
||||
|
||||
// server/api/users/[id].delete.ts - DELETE /api/users/:id
|
||||
export default defineEventHandler((event) => {
|
||||
const id = getRouterParam(event, 'id')
|
||||
return deleteUser(id)
|
||||
})
|
||||
const id = getRouterParam(event, 'id');
|
||||
return deleteUser(id);
|
||||
});
|
||||
```
|
||||
|
||||
### Route Parameters
|
||||
@@ -53,15 +53,15 @@ export default defineEventHandler((event) => {
|
||||
```ts
|
||||
// server/api/posts/[id].ts
|
||||
export default defineEventHandler((event) => {
|
||||
const id = getRouterParam(event, 'id')
|
||||
return getPost(id)
|
||||
})
|
||||
const id = getRouterParam(event, 'id');
|
||||
return getPost(id);
|
||||
});
|
||||
|
||||
// Catch-all: server/api/[...path].ts
|
||||
export default defineEventHandler((event) => {
|
||||
const path = getRouterParam(event, 'path')
|
||||
return { path }
|
||||
})
|
||||
const path = getRouterParam(event, 'path');
|
||||
return { path };
|
||||
});
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
@@ -70,10 +70,10 @@ export default defineEventHandler((event) => {
|
||||
// server/api/search.ts
|
||||
// GET /api/search?q=nuxt&page=1
|
||||
export default defineEventHandler((event) => {
|
||||
const query = getQuery(event)
|
||||
const query = getQuery(event);
|
||||
// { q: 'nuxt', page: '1' }
|
||||
return search(query.q, Number(query.page))
|
||||
})
|
||||
return search(query.q, Number(query.page));
|
||||
});
|
||||
```
|
||||
|
||||
### Request Body
|
||||
@@ -81,10 +81,10 @@ export default defineEventHandler((event) => {
|
||||
```ts
|
||||
// server/api/submit.post.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
const body = await readBody(event);
|
||||
// Validate and process body
|
||||
return { success: true, data: body }
|
||||
})
|
||||
return { success: true, data: body };
|
||||
});
|
||||
```
|
||||
|
||||
### Headers and Cookies
|
||||
@@ -93,24 +93,24 @@ export default defineEventHandler(async (event) => {
|
||||
// server/api/auth.ts
|
||||
export default defineEventHandler((event) => {
|
||||
// Read headers
|
||||
const auth = getHeader(event, 'authorization')
|
||||
const auth = getHeader(event, 'authorization');
|
||||
|
||||
// Read cookies
|
||||
const cookies = parseCookies(event)
|
||||
const token = getCookie(event, 'token')
|
||||
const cookies = parseCookies(event);
|
||||
const token = getCookie(event, 'token');
|
||||
|
||||
// Set headers
|
||||
setHeader(event, 'X-Custom-Header', 'value')
|
||||
setHeader(event, 'X-Custom-Header', 'value');
|
||||
|
||||
// Set cookies
|
||||
setCookie(event, 'token', 'new-token', {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
maxAge: 60 * 60 * 24, // 1 day
|
||||
})
|
||||
});
|
||||
|
||||
return { authenticated: !!token }
|
||||
})
|
||||
return { authenticated: !!token };
|
||||
});
|
||||
```
|
||||
|
||||
## Server Middleware
|
||||
@@ -120,16 +120,16 @@ Runs on every request before routes:
|
||||
```ts
|
||||
// server/middleware/auth.ts
|
||||
export default defineEventHandler((event) => {
|
||||
const token = getCookie(event, 'token')
|
||||
const token = getCookie(event, 'token');
|
||||
|
||||
// Attach data to event context
|
||||
event.context.user = token ? verifyToken(token) : null
|
||||
})
|
||||
event.context.user = token ? verifyToken(token) : null;
|
||||
});
|
||||
|
||||
// server/middleware/log.ts
|
||||
export default defineEventHandler((event) => {
|
||||
console.log(`${event.method} ${event.path}`)
|
||||
})
|
||||
console.log(`${event.method} ${event.path}`);
|
||||
});
|
||||
```
|
||||
|
||||
Access context in routes:
|
||||
@@ -137,12 +137,12 @@ Access context in routes:
|
||||
```ts
|
||||
// server/api/profile.ts
|
||||
export default defineEventHandler((event) => {
|
||||
const user = event.context.user
|
||||
const user = event.context.user;
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
||||
throw createError({ statusCode: 401, message: 'Unauthorized' });
|
||||
}
|
||||
return user
|
||||
})
|
||||
return user;
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
@@ -150,18 +150,18 @@ export default defineEventHandler((event) => {
|
||||
```ts
|
||||
// server/api/users/[id].ts
|
||||
export default defineEventHandler((event) => {
|
||||
const id = getRouterParam(event, 'id')
|
||||
const user = findUser(id)
|
||||
const id = getRouterParam(event, 'id');
|
||||
const user = findUser(id);
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'User not found',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return user
|
||||
})
|
||||
return user;
|
||||
});
|
||||
```
|
||||
|
||||
## Server Utils
|
||||
@@ -171,16 +171,16 @@ Auto-imported in `server/utils/`:
|
||||
```ts
|
||||
// server/utils/db.ts
|
||||
export function useDb() {
|
||||
return createDbConnection()
|
||||
return createDbConnection();
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// server/api/users.ts
|
||||
export default defineEventHandler(() => {
|
||||
const db = useDb() // Auto-imported
|
||||
return db.query('SELECT * FROM users')
|
||||
})
|
||||
const db = useDb(); // Auto-imported
|
||||
return db.query('SELECT * FROM users');
|
||||
});
|
||||
```
|
||||
|
||||
## Server Plugins
|
||||
@@ -191,13 +191,13 @@ Run once when server starts:
|
||||
// server/plugins/db.ts
|
||||
export default defineNitroPlugin((nitroApp) => {
|
||||
// Initialize database connection
|
||||
const db = createDbConnection()
|
||||
const db = createDbConnection();
|
||||
|
||||
// Add to context
|
||||
nitroApp.hooks.hook('request', (event) => {
|
||||
event.context.db = db
|
||||
})
|
||||
})
|
||||
event.context.db = db;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Streaming Responses
|
||||
@@ -205,22 +205,22 @@ export default defineNitroPlugin((nitroApp) => {
|
||||
```ts
|
||||
// server/api/stream.ts
|
||||
export default defineEventHandler((event) => {
|
||||
setHeader(event, 'Content-Type', 'text/event-stream')
|
||||
setHeader(event, 'Cache-Control', 'no-cache')
|
||||
setHeader(event, 'Connection', 'keep-alive')
|
||||
setHeader(event, 'Content-Type', 'text/event-stream');
|
||||
setHeader(event, 'Cache-Control', 'no-cache');
|
||||
setHeader(event, 'Connection', 'keep-alive');
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
controller.enqueue(`data: ${JSON.stringify({ count: i })}\n\n`)
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
controller.enqueue(`data: ${JSON.stringify({ count: i })}\n\n`);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
controller.close()
|
||||
controller.close();
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return stream
|
||||
})
|
||||
return stream;
|
||||
});
|
||||
```
|
||||
|
||||
## Server Storage
|
||||
@@ -230,16 +230,16 @@ Key-value storage with multiple drivers:
|
||||
```ts
|
||||
// server/api/cache.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const storage = useStorage()
|
||||
const storage = useStorage();
|
||||
|
||||
// Set value
|
||||
await storage.setItem('key', { data: 'value' })
|
||||
await storage.setItem('key', { data: 'value' });
|
||||
|
||||
// Get value
|
||||
const data = await storage.getItem('key')
|
||||
const data = await storage.getItem('key');
|
||||
|
||||
return data
|
||||
})
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
Configure storage drivers in `nuxt.config.ts`:
|
||||
@@ -254,10 +254,10 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/getting-started/server
|
||||
- https://nuxt.com/docs/directory-structure/server
|
||||
|
||||
@@ -14,7 +14,7 @@ SSR-safe replacement for `ref` that shares state across components:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// State is shared by key 'counter' across all components
|
||||
const counter = useState('counter', () => 0)
|
||||
const counter = useState('counter', () => 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -33,19 +33,19 @@ Define reusable state composables:
|
||||
```ts
|
||||
// composables/useUser.ts
|
||||
export function useUser() {
|
||||
return useState<User | null>('user', () => null)
|
||||
return useState<User | null>('user', () => null);
|
||||
}
|
||||
|
||||
export function useLocale() {
|
||||
return useState('locale', () => 'en')
|
||||
return useState('locale', () => 'en');
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// Same state instance everywhere
|
||||
const user = useUser()
|
||||
const locale = useLocale()
|
||||
const user = useUser();
|
||||
const locale = useLocale();
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -55,11 +55,11 @@ Use `callOnce` to initialize state with async data:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const config = useState('site-config')
|
||||
const config = useState('site-config');
|
||||
|
||||
await callOnce(async () => {
|
||||
config.value = await $fetch('/api/config')
|
||||
})
|
||||
config.value = await $fetch('/api/config');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -69,27 +69,27 @@ await callOnce(async () => {
|
||||
|
||||
```ts
|
||||
// ❌ Wrong - causes memory leaks and shared state across requests
|
||||
export const globalState = ref({ user: null })
|
||||
export const globalState = ref({ user: null });
|
||||
```
|
||||
|
||||
### ✅ Use useState Instead
|
||||
|
||||
```ts
|
||||
// ✅ Correct - SSR-safe
|
||||
export const useGlobalState = () => useState('global', () => ({ user: null }))
|
||||
export const useGlobalState = () => useState('global', () => ({ user: null }));
|
||||
```
|
||||
|
||||
## Clearing State
|
||||
|
||||
```ts
|
||||
// Clear specific state
|
||||
clearNuxtState('counter')
|
||||
clearNuxtState('counter');
|
||||
|
||||
// Clear multiple states
|
||||
clearNuxtState(['counter', 'user'])
|
||||
clearNuxtState(['counter', 'user']);
|
||||
|
||||
// Clear all state (use with caution)
|
||||
clearNuxtState()
|
||||
clearNuxtState();
|
||||
```
|
||||
|
||||
## With Pinia
|
||||
@@ -108,38 +108,38 @@ export const useCounterStore = defineStore('counter', {
|
||||
}),
|
||||
actions: {
|
||||
increment() {
|
||||
this.count++
|
||||
this.count++;
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// stores/user.ts (Composition API style)
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const user = ref<User | null>(null)
|
||||
const isLoggedIn = computed(() => !!user.value)
|
||||
const user = ref<User | null>(null);
|
||||
const isLoggedIn = computed(() => !!user.value);
|
||||
|
||||
async function login(credentials: Credentials) {
|
||||
user.value = await $fetch('/api/login', {
|
||||
method: 'POST',
|
||||
body: credentials,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return { user, isLoggedIn, login }
|
||||
})
|
||||
return { user, isLoggedIn, login };
|
||||
});
|
||||
```
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const counterStore = useCounterStore()
|
||||
const userStore = useUserStore()
|
||||
const counterStore = useCounterStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// Initialize store data once
|
||||
await callOnce(async () => {
|
||||
await userStore.fetchUser()
|
||||
})
|
||||
await userStore.fetchUser();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -148,22 +148,21 @@ await callOnce(async () => {
|
||||
```ts
|
||||
// composables/useLocale.ts
|
||||
export function useLocale() {
|
||||
return useState('locale', () => useDefaultLocale().value)
|
||||
return useState('locale', () => useDefaultLocale().value);
|
||||
}
|
||||
|
||||
export function useDefaultLocale(fallback = 'en-US') {
|
||||
const locale = ref(fallback)
|
||||
const locale = ref(fallback);
|
||||
|
||||
if (import.meta.server) {
|
||||
const reqLocale = useRequestHeaders()['accept-language']?.split(',')[0]
|
||||
if (reqLocale) locale.value = reqLocale
|
||||
}
|
||||
else if (import.meta.client) {
|
||||
const navLang = navigator.language
|
||||
if (navLang) locale.value = navLang
|
||||
const reqLocale = useRequestHeaders()['accept-language']?.split(',')[0];
|
||||
if (reqLocale) locale.value = reqLocale;
|
||||
} else if (import.meta.client) {
|
||||
const navLang = navigator.language;
|
||||
if (navLang) locale.value = navLang;
|
||||
}
|
||||
|
||||
return locale
|
||||
return locale;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -178,15 +177,15 @@ export function useDefaultLocale(fallback = 'en-US') {
|
||||
|
||||
```ts
|
||||
// ❌ Won't work
|
||||
useState('fn', () => () => console.log('hi'))
|
||||
useState('instance', () => new MyClass())
|
||||
useState('fn', () => () => console.log('hi'));
|
||||
useState('instance', () => new MyClass());
|
||||
|
||||
// ✅ Works
|
||||
useState('data', () => ({ name: 'John', age: 30 }))
|
||||
useState('items', () => ['a', 'b', 'c'])
|
||||
useState('data', () => ({ name: 'John', age: 30 }));
|
||||
useState('items', () => ['a', 'b', 'c']);
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/getting-started/state-management
|
||||
- https://nuxt.com/docs/api/composables/use-state
|
||||
|
||||
@@ -15,15 +15,17 @@ Server renders HTML, then hydrates on client:
|
||||
// nuxt.config.ts - this is the default
|
||||
export default defineNuxtConfig({
|
||||
ssr: true,
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Fast initial page load (HTML is ready)
|
||||
- SEO-friendly (content is in HTML)
|
||||
- Works without JavaScript initially
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Server executes Vue code, generates HTML
|
||||
2. Browser displays HTML immediately
|
||||
3. JavaScript loads and hydrates the page
|
||||
@@ -37,15 +39,17 @@ Render entirely in the browser:
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Simpler development (no SSR constraints)
|
||||
- Cheaper hosting (static files only)
|
||||
- Works offline
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Admin dashboards
|
||||
- SaaS applications
|
||||
- Apps behind authentication
|
||||
@@ -62,24 +66,26 @@ Provide loading UI while app hydrates:
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #00dc82;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #00dc82;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
@@ -106,21 +112,21 @@ export default defineNuxtConfig({
|
||||
// Server-rendered (default)
|
||||
'/api/**': { cors: true },
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Route Rules Reference
|
||||
|
||||
| Rule | Description |
|
||||
|------|-------------|
|
||||
| `prerender: true` | Pre-render at build time |
|
||||
| `ssr: false` | Client-side only |
|
||||
| `swr: number \| true` | Stale-while-revalidate caching |
|
||||
| `isr: number \| true` | Incremental static regeneration |
|
||||
| `cache: { maxAge: number }` | Cache with TTL |
|
||||
| `redirect: string` | Redirect to another path |
|
||||
| `cors: true` | Add CORS headers |
|
||||
| `headers: object` | Custom response headers |
|
||||
| Rule | Description |
|
||||
| --------------------------- | ------------------------------- |
|
||||
| `prerender: true` | Pre-render at build time |
|
||||
| `ssr: false` | Client-side only |
|
||||
| `swr: number \| true` | Stale-while-revalidate caching |
|
||||
| `isr: number \| true` | Incremental static regeneration |
|
||||
| `cache: { maxAge: number }` | Cache with TTL |
|
||||
| `redirect: string` | Redirect to another path |
|
||||
| `cors: true` | Add CORS headers |
|
||||
| `headers: object` | Custom response headers |
|
||||
|
||||
### Inline Route Rules
|
||||
|
||||
@@ -130,7 +136,7 @@ Define per-page:
|
||||
<script setup lang="ts">
|
||||
defineRouteRules({
|
||||
prerender: true,
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -147,7 +153,7 @@ export default defineNuxtConfig({
|
||||
'/about': { prerender: true },
|
||||
'/posts/*': { prerender: true },
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Or use `nuxt generate`:
|
||||
@@ -164,20 +170,20 @@ export default defineNuxtConfig({
|
||||
hooks: {
|
||||
'prerender:routes'({ routes }) {
|
||||
// Add dynamic routes
|
||||
const posts = await fetchPostSlugs()
|
||||
const posts = await fetchPostSlugs();
|
||||
for (const slug of posts) {
|
||||
routes.add(`/posts/${slug}`)
|
||||
routes.add(`/posts/${slug}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Or in pages:
|
||||
|
||||
```ts
|
||||
// server/api/posts.ts or a plugin
|
||||
prerenderRoutes(['/posts/1', '/posts/2', '/posts/3'])
|
||||
prerenderRoutes(['/posts/1', '/posts/2', '/posts/3']);
|
||||
```
|
||||
|
||||
## Edge-Side Rendering
|
||||
@@ -190,10 +196,11 @@ export default defineNuxtConfig({
|
||||
nitro: {
|
||||
preset: 'cloudflare-pages', // or 'vercel-edge', 'netlify-edge'
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Supported platforms:
|
||||
|
||||
- Cloudflare Pages/Workers
|
||||
- Vercel Edge Functions
|
||||
- Netlify Edge Functions
|
||||
@@ -206,12 +213,12 @@ Use `import.meta.server` and `import.meta.client`:
|
||||
<script setup>
|
||||
if (import.meta.server) {
|
||||
// Server-only code
|
||||
console.log('Running on server')
|
||||
console.log('Running on server');
|
||||
}
|
||||
|
||||
if (import.meta.client) {
|
||||
// Client-only code
|
||||
console.log('Running in browser')
|
||||
console.log('Running in browser');
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -229,7 +236,7 @@ For components:
|
||||
</template>
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://nuxt.com/docs/guide/concepts/rendering
|
||||
- https://nuxt.com/docs/getting-started/prerendering
|
||||
|
||||
Reference in New Issue
Block a user