@gallopsystems/agent-skills 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +1 -1
  4. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +23 -8
  5. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +1 -0
  6. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/caching.md +68 -0
  7. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +31 -1
  8. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/error-handling.md +74 -0
  9. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +18 -0
  10. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/formatters.md +11 -0
  11. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/layers.md +40 -0
  12. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +1 -0
  13. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/route-rules.md +41 -0
  14. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-runtime.md +97 -0
  15. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +60 -0
  16. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/state-management.md +68 -0
  17. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/storage.md +59 -0
  18. package/plugins/volt-primevue/skills/volt-primevue/SKILL.md +56 -12
  19. package/plugins/volt-primevue/skills/volt-primevue/config.md +51 -0
  20. package/plugins/volt-primevue/skills/volt-primevue/gotchas.md +49 -0
  21. package/plugins/volt-primevue/skills/volt-primevue/theming.md +20 -6
  22. package/plugins/vue-nuxt/.claude-plugin/plugin.json +8 -0
  23. package/plugins/vue-nuxt/skills/vue-nuxt/SKILL.md +48 -0
  24. package/plugins/vue-nuxt/skills/vue-nuxt/auto-imports.md +48 -0
  25. package/plugins/vue-nuxt/skills/vue-nuxt/component-authoring.md +159 -0
  26. package/plugins/vue-nuxt/skills/vue-nuxt/composables.md +95 -0
  27. package/plugins/vue-nuxt/skills/vue-nuxt/reactivity.md +133 -0
  28. package/plugins/vue-nuxt/skills/vue-nuxt/slots.md +139 -0
  29. package/plugins/vue-nuxt/skills/vue-nuxt/template-idioms.md +142 -0
  30. package/plugins/vue-nuxt/skills/vue-nuxt/v-model.md +106 -0
  31. package/plugins/vue-nuxt/skills/vue-nuxt/watch.md +194 -0
package/README.md CHANGED
@@ -19,6 +19,7 @@ Then install the skills you want:
19
19
  /plugin install git-github@gallop-systems-agent-skills
20
20
  /plugin install copier-template@gallop-systems-agent-skills
21
21
  /plugin install volt-primevue@gallop-systems-agent-skills
22
+ /plugin install vue-nuxt@gallop-systems-agent-skills
22
23
  ```
23
24
 
24
25
  ## Updating
@@ -87,7 +88,7 @@ Covers:
87
88
 
88
89
  ### nuxt-nitro-api
89
90
 
90
- Nuxt 3 / Nitro API patterns for building type-safe full-stack applications. Automatically activates when working in Nuxt 3 projects.
91
+ Nuxt 4 / Nitro API patterns for building type-safe full-stack applications. Automatically activates when working in Nuxt 4 projects.
91
92
 
92
93
  Covers:
93
94
  - Zod validation with h3 (Standard Schema support)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gallopsystems/agent-skills",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Gallop Systems Claude Code skills, symlinked into .claude/skills on install.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-nitro-api",
3
- "description": "Nuxt 3 / Nitro API patterns: validation, auth, SSR, tasks, SSE",
3
+ "description": "Nuxt 4 / Nitro API patterns: validation, auth, SSR, tasks, SSE",
4
4
  "version": "1.0.0",
5
5
  "author": {
6
6
  "name": "yeedle"
@@ -1,16 +1,22 @@
1
1
  ---
2
2
  name: nuxt-nitro-api
3
- description: Build type-safe Nuxt 3 applications with Nitro API patterns. Covers validation, fetch patterns, auth, SSR, composables, background tasks, and real-time features.
3
+ description: Build type-safe Nuxt 4 applications with Nitro API patterns. Covers validation, fetch patterns, auth, SSR, composables, background tasks, and real-time features.
4
4
  ---
5
5
 
6
- # Nuxt 3 / Nitro API Patterns
6
+ # Nuxt 4 / Nitro API Patterns
7
7
 
8
- This skill provides patterns for building type-safe Nuxt 3 applications with Nitro backends.
8
+ This skill provides patterns for building type-safe Nuxt applications with Nitro backends.
9
+
10
+ > **Version note:** Nitro's version follows Nuxt — **Nuxt 4.x ships Nitro v2**
11
+ > (`nitropack ^2.x`, via `@nuxt/nitro-server`; you have no direct `nitropack`
12
+ > dependency). `nitro.build` now documents **Nitro v3**, which renames core APIs;
13
+ > copying its examples verbatim breaks here. See [server-runtime.md](./server-runtime.md)
14
+ > for the v2↔v3 mapping.
9
15
 
10
16
  ## When to Use This Skill
11
17
 
12
18
  Use this skill when:
13
- - Working in a Nuxt 3 project with TypeScript
19
+ - Working in a Nuxt 4 project with TypeScript
14
20
  - Building API endpoints with Nitro
15
21
  - Implementing authentication with nuxt-auth-utils
16
22
  - Handling SSR + client-side state
@@ -21,13 +27,20 @@ Use this skill when:
21
27
  For detailed patterns, see these topic-focused reference files:
22
28
 
23
29
  - [validation.md](./validation.md) - Zod validation with h3, Standard Schema, error handling
24
- - [fetch-patterns.md](./fetch-patterns.md) - useFetch vs $fetch vs useAsyncData
30
+ - [fetch-patterns.md](./fetch-patterns.md) - useFetch vs $fetch vs useAsyncData, refreshNuxtData
25
31
  - [auth-patterns.md](./auth-patterns.md) - nuxt-auth-utils, OAuth, WebAuthn, middleware
32
+ - [error-handling.md](./error-handling.md) - createError (fatal), error.vue, clearError, NuxtErrorBoundary
33
+ - [state-management.md](./state-management.md) - useState (never a module-scope ref), clearNuxtState, callOnce
26
34
  - [page-structure.md](./page-structure.md) - Keep pages thin, components do the work
27
- - [composables-utils.md](./composables-utils.md) - When to use composables vs utils
35
+ - [composables-utils.md](./composables-utils.md) - composables vs utils, runtimeConfig public/private, runWithContext
28
36
  - [formatters.md](./formatters.md) - Centralize currency/date/number formatters in useFormatters, never inline
29
- - [ssr-client.md](./ssr-client.md) - SSR + localStorage, hydration, VueUse
37
+ - [ssr-client.md](./ssr-client.md) - SSR + localStorage, hydration, useRequestURL/Headers, useCookie, VueUse
30
38
  - [deep-linking.md](./deep-linking.md) - URL params sync with filters and useFetch
39
+ - [caching.md](./caching.md) - defineCachedFunction/EventHandler, SWR, per-key invalidation (Nitro v2)
40
+ - [storage.md](./storage.md) - useStorage / unstorage KV layer, mounts
41
+ - [route-rules.md](./route-rules.md) - declarative cache/headers/redirect/proxy/CORS per path
42
+ - [server-runtime.md](./server-runtime.md) - **Nitro v2 vs v3 version pin**, middleware order, useEvent, internal-$fetch auth, WebSockets
43
+ - [layers.md](./layers.md) - sharing components/composables/config across repos via extends
31
44
  - [nitro-tasks.md](./nitro-tasks.md) - Background jobs, scheduled tasks, job queues
32
45
  - [sse.md](./sse.md) - Server-Sent Events for real-time streaming
33
46
  - [server-services.md](./server-services.md) - Third-party service integration patterns
@@ -72,7 +85,7 @@ From nuxt-auth-utils:
72
85
  All auto-imported:
73
86
  - Vue: `ref`, `computed`, `watch`, `onMounted`, etc.
74
87
  - VueUse: `refDebounced`, `useLocalStorage`, `useUrlSearchParams`, etc.
75
- - Nuxt: `useFetch`, `useAsyncData`, `useRoute`, `useRouter`, `useState`, `navigateTo`
88
+ - Nuxt: `useFetch`, `useAsyncData`, `useRoute`, `useRouter`, `useState`, `navigateTo`, `callOnce`, `refreshNuxtData`, `clearNuxtState`, `useRequestURL`, `useRequestHeaders`, `useCookie`, `createError`, `showError`, `clearError`, `useNuxtApp`
76
89
 
77
90
  ### Shared (`/shared` directory - Nuxt 3.14+)
78
91
 
@@ -209,6 +222,8 @@ Needs Nuxt/Vue context (useRuntimeConfig, useRoute, refs)?
209
222
  8. **Cookie size limit is 4096 bytes** - Store only essential session data.
210
223
  9. **Ambiguous routes need type assertion** - See below.
211
224
  10. **Never use generic type params with useFetch/$fetch** - See below.
225
+ 11. **Never export a module-scope `ref` for shared state** - Leaks across SSR requests; use `useState`. See [state-management.md](./state-management.md).
226
+ 12. **Internal server `$fetch` doesn't forward cookies** - Pass `headers` explicitly or the callee sees no session. See [server-runtime.md](./server-runtime.md).
212
227
 
213
228
  ### Ambiguous Route Type Inference
214
229
 
@@ -226,3 +226,4 @@ NUXT_OAUTH_GOOGLE_CLIENT_SECRET=...
226
226
  4. **setUserSession merges** - Use `replaceUserSession` to replace
227
227
  5. **requireUserSession throws** - Use getUserSession for null
228
228
  6. **Cannot use with `nuxt generate`** - Requires running server
229
+ 7. **`navigateTo` external routes need `{ external: true }`** - In-app routes: `navigateTo('/dashboard')`. Server/OAuth endpoints that aren't Vue routes (e.g. `/api/auth/google`, `/api/auth/preview-login`) must pass `navigateTo('/api/auth/google', { external: true })`, or Vue Router tries to resolve them as client routes and fails. Build query routes with `navigateTo({ path, query })` and `String()` any numeric ids.
@@ -0,0 +1,68 @@
1
+ # Response & Function Caching (Nitro)
2
+
3
+ Nitro ships a KV-backed cache layer with stale-while-revalidate and per-key
4
+ invalidation. Reach for it before adding a memoization Map or a Postgres cache
5
+ table. All helpers are **auto-imported in `server/`**.
6
+
7
+ > **Nitro v2 names** (this project — see [server-runtime.md](./server-runtime.md)).
8
+ > `nitro.build` documents v3, which moves these to `import { … } from "nitro/cache"`.
9
+ > Here they are auto-imported globals.
10
+
11
+ ## `defineCachedFunction` — cache an expensive async function
12
+
13
+ ```typescript
14
+ const getStars = defineCachedFunction(
15
+ async (repo: string) => {
16
+ const { stargazers_count } = await $fetch(`https://api.github.com/repos/${repo}`);
17
+ return stargazers_count as number;
18
+ },
19
+ {
20
+ maxAge: 60 * 60, // serve from cache for 1h
21
+ swr: true, // after maxAge, serve stale and revalidate in the background
22
+ name: "ghStars",
23
+ group: "nitro/functions",
24
+ getKey: (repo) => repo, // cache key from the args
25
+ },
26
+ );
27
+
28
+ await getStars("unjs/nitro");
29
+ await getStars.invalidate("unjs/nitro"); // drop one key on demand (e.g. after a mutation)
30
+ ```
31
+
32
+ `getKey` is what makes it safe to call with different arguments — without it,
33
+ every arg shares one cache entry.
34
+
35
+ ## `defineCachedEventHandler` — cache a whole endpoint
36
+
37
+ ```typescript
38
+ export default defineCachedEventHandler(
39
+ async (event) => ({ now: Date.now(), data: await loadData() }),
40
+ {
41
+ maxAge: 60,
42
+ swr: true,
43
+ varies: ["host"], // vary the key on these headers
44
+ shouldBypassCache: (event) => !!getQuery(event).fresh, // skip cache for ?fresh=1
45
+ getKey: (event) => event.path,
46
+ },
47
+ );
48
+ ```
49
+
50
+ Don't cache an endpoint whose body depends on the session/user unless you `vary`
51
+ on the discriminator — otherwise one user's response is served to another.
52
+
53
+ ## Where the cache lives
54
+
55
+ The cache backs onto the `cache` **storage mount** (see [storage.md](./storage.md)):
56
+ filesystem in dev, whatever you mount (Redis, etc.) in production. Configure via
57
+ `nitro.storage` / `nitro.devStorage` in `nuxt.config.ts`.
58
+
59
+ ## Gotchas
60
+
61
+ - **`maxAge` is seconds, not ms.**
62
+ - **`swr: true`** means a request after expiry gets the *stale* value immediately
63
+ while a background refresh runs — great for latency, but readers can see
64
+ slightly old data. Omit it for must-be-fresh endpoints.
65
+ - **Invalidate on write.** After a mutation changes the source data, call
66
+ `fn.invalidate(key)` — caches don't know your DB changed.
67
+ - **Route-level caching** for simple cases can be declared without a handler
68
+ wrapper via `routeRules` (`swr` / `cache`) — see [route-rules.md](./route-rules.md).
@@ -161,6 +161,35 @@ export function formatCurrency(amount: number) {
161
161
  // - /pages/invoice.vue
162
162
  ```
163
163
 
164
+ ## `useRuntimeConfig`: `public` on the client, private on the server
165
+
166
+ Only `config.public.*` exists in client code. Reading a private (top-level) key
167
+ in the browser returns an empty string — a silent bug, not an error:
168
+
169
+ ```typescript
170
+ const config = useRuntimeConfig();
171
+ config.public.apiBase; // ✅ client + server
172
+ config.apiSecret; // ✅ server only — ❌ empty on the client
173
+ ```
174
+
175
+ Env binding follows the split: `NUXT_*` → server keys, `NUXT_PUBLIC_*` → public
176
+ keys. Put anything a browser must never see under a private (non-`public`) key.
177
+
178
+ ## Calling a composable after `await`: `runWithContext`
179
+
180
+ Composables and `navigateTo` must run in Nuxt's context. After an `await`, that
181
+ context can be lost — the classic **"Nuxt instance unavailable"** error. Restore
182
+ it with `nuxtApp.runWithContext`:
183
+
184
+ ```typescript
185
+ const nuxtApp = useNuxtApp();
186
+ const result = await someAsyncWork();
187
+ return nuxtApp.runWithContext(() => navigateTo("/next")); // ✅ context restored post-await
188
+ ```
189
+
190
+ (Capture `useNuxtApp()`/the composable result *before* the await when you can;
191
+ `runWithContext` is the escape hatch for when you can't.)
192
+
164
193
  ## Summary Table
165
194
 
166
195
  | Location | Naming | Vue APIs | Auto-imported | Use Case |
@@ -176,4 +205,5 @@ export function formatCurrency(amount: number) {
176
205
  2. **Don't use Vue APIs in utils** - Keeps them testable and portable
177
206
  3. **Server utils can't use Vue** - Different runtime
178
207
  4. **Auto-import scoping** - `/utils` is client-only, `/server/utils` is server-only
179
- 5. **Composables call order matters** - Call at top of `<script setup>`, not in callbacks
208
+ 5. **Composables call order matters** - Call at top of `<script setup>`, not in callbacks, and **never inside a template expression** - a composable invoked from the template (or a callback) runs outside the request/setup scope and throws "must be called at the top of setup". Call it once in setup, then use the returned helpers/refs in the template.
209
+ 6. **Guard possibly-null data when deriving** - Composables like `useUserSession` expose refs that can be `null` (logged out, still loading). Derive display state through a `computed` with a null guard (`computed(() => user.value ? getInitials(user.value) : '')`), not a bare property access that assumes presence.
@@ -0,0 +1,74 @@
1
+ # Error Handling (client + server)
2
+
3
+ The skill covers throwing `createError` in API handlers (see `auth-patterns.md`).
4
+ This file is the **client/page** surface: triggering the error page, the
5
+ `error.vue` root, recovering, and containing a failure to a subtree.
6
+
7
+ ## `createError` — fatal vs non-fatal
8
+
9
+ `createError` works on both sides. On the **server** it sets the HTTP status. On
10
+ the **client/page**, a `createError` with `fatal: true` replaces the whole page
11
+ with the error page (`error.vue`); without `fatal`, it's a non-fatal error you
12
+ handle locally:
13
+
14
+ ```typescript
15
+ // page setup — show the full-screen error page for a missing resource
16
+ const { data } = await useFetch(`/api/invoices/${id}`);
17
+ if (!data.value) {
18
+ throw createError({ statusCode: 404, statusMessage: "Invoice not found", fatal: true });
19
+ }
20
+ ```
21
+
22
+ `showError(...)` is the imperative equivalent (call it from a handler instead of
23
+ throwing). Pass `data` on the error to carry structured detail to `error.vue`.
24
+
25
+ ## `error.vue` — the app-root error page
26
+
27
+ A single `error.vue` at the project root renders for any fatal error (and SSR
28
+ 500s). It receives the error as a prop and clears it with `clearError`:
29
+
30
+ ```vue
31
+ <script setup lang="ts">
32
+ const props = defineProps<{ error: NuxtError }>();
33
+ // clearError unmounts the error page; redirect clears the errored route
34
+ const handled = () => clearError({ redirect: "/" });
35
+ </script>
36
+
37
+ <template>
38
+ <div>
39
+ <h1>{{ error.statusCode }}</h1>
40
+ <p>{{ error.statusMessage }}</p>
41
+ <button @click="handled">Go home</button>
42
+ </div>
43
+ </template>
44
+ ```
45
+
46
+ Only `clearError` dismisses the error page — re-rendering won't. Gotcha: on the
47
+ error page, **middleware re-runs** but **plugins do not re-run** until you
48
+ `clearError()`, so don't rely on plugin-provided state inside `error.vue`.
49
+
50
+ ## `<NuxtErrorBoundary>` — contain a failure to a subtree
51
+
52
+ When one widget can fail without taking down the page (a flaky third-party embed,
53
+ an optional panel), wrap it so the error is caught locally instead of bubbling to
54
+ `error.vue`:
55
+
56
+ ```vue
57
+ <NuxtErrorBoundary @error="logError">
58
+ <RiskyWidget />
59
+ <template #error="{ error, clearError }">
60
+ <p>Couldn't load this section.</p>
61
+ <button @click="clearError">Retry</button>
62
+ </template>
63
+ </NuxtErrorBoundary>
64
+ ```
65
+
66
+ ## Which to reach for
67
+
68
+ | Situation | Use |
69
+ |---|---|
70
+ | Resource missing / unauthorized in page setup | `throw createError({ …, fatal: true })` |
71
+ | Imperatively show the error page from a handler | `showError(...)` |
72
+ | Render for any fatal error, app-wide | `error.vue` + `clearError` |
73
+ | One section may fail without killing the page | `<NuxtErrorBoundary>` |
74
+ | API handler rejecting a request | `createError` (status code) — see `auth-patterns.md` |
@@ -121,6 +121,24 @@ const handleDelete = async (id: number) => {
121
121
  };
122
122
  ```
123
123
 
124
+ ## Invalidating after a mutation
125
+
126
+ `refresh()` re-pulls a single `useFetch`. When a mutation affects data loaded by
127
+ **other** components, use the global `refreshNuxtData(keys?)` instead of wiring
128
+ cross-component refresh plumbing — it re-runs every matching `useFetch`/
129
+ `useAsyncData` payload:
130
+
131
+ ```typescript
132
+ await $fetch("/api/invoices", { method: "POST", body });
133
+ await refreshNuxtData(["invoices", "invoice-summary"]); // re-pull by explicit key
134
+ // refreshNuxtData() with no args refetches everything (use sparingly)
135
+ ```
136
+
137
+ For this to target precisely, give the fetches explicit keys
138
+ (`useAsyncData("invoices", …)` / `useFetch("/api/invoices", { key: "invoices" })`).
139
+ `clearNuxtData(keys?)` drops cached payload + error state without refetching (e.g.
140
+ reset a wizard's loaded data on cancel).
141
+
124
142
  ## Type Inference
125
143
 
126
144
  Template literals preserve type inference (fixed late 2024):
@@ -71,6 +71,17 @@ or date logic by hand:
71
71
  Still route date-fns calls through `useFormatters` (or a shared util) rather than
72
72
  importing and calling them inline everywhere — same single-source-of-truth reason.
73
73
 
74
+ ## DATE columns vs timestamps: keep date-only values as strings
75
+
76
+ Postgres `timestamp`/`timestamptz` values are serialized as ISO strings over the
77
+ wire — rehydrate them with a `useFetch` `transform` (or `parseISO` at the edge) so
78
+ the rest of the app holds real `Date`s. But a **`DATE` column** (a calendar day
79
+ with no time — a due date, a birthday) must stay a plain `"2026-06-14"` string:
80
+ wrapping it in `new Date("2026-06-14")` parses it as **UTC midnight**, which
81
+ renders as the *previous day* in any negative-offset timezone. Format date-only
82
+ strings with `parseISO` (which treats them as local), never `new Date`, and don't
83
+ let a blanket "convert all date fields to Date" transform touch DATE columns.
84
+
74
85
  ## Where to put the formatters
75
86
 
76
87
  Pick the location by what the formatter needs (see [composables-utils.md](./composables-utils.md)):
@@ -0,0 +1,40 @@
1
+ # Nuxt Layers
2
+
3
+ Layers let multiple Nuxt projects share components, composables, utils, server
4
+ routes, and config. Relevant when a base/template repo feeds several apps — e.g. a
5
+ copier-scaffolded house style, or a shared design system across products.
6
+
7
+ ## Extending
8
+
9
+ ```typescript
10
+ // nuxt.config.ts
11
+ export default defineNuxtConfig({
12
+ extends: [
13
+ "../base-layer", // local path
14
+ "@my-org/nuxt-theme", // npm package
15
+ ["github:my-org/layer", { install: true }], // remote
16
+ ],
17
+ });
18
+ ```
19
+
20
+ Auto-imports work **across** layers — a composable in the base layer is available
21
+ unprefixed in the consumer, same as a local one.
22
+
23
+ ## Precedence
24
+
25
+ Project root **overrides** `~~/layers/*` (alphabetical) **overrides** `extends`
26
+ entries (in listed order). So a consumer can shadow any layer file by placing a
27
+ file at the same path. Local `~~/layers/*` directories are auto-registered without
28
+ listing them in `extends`; they also get `#layers/*` aliases.
29
+
30
+ ## When a layer vs a package vs the agent-skills repo
31
+
32
+ | Sharing | Use |
33
+ |---|---|
34
+ | Runtime Nuxt surface (components, composables, server routes, config) | a **layer** (`extends`) |
35
+ | Framework-agnostic JS/TS logic | a plain npm package |
36
+ | Agent guidance / Claude skills | the `@gallopsystems/agent-skills` package |
37
+
38
+ Don't reach for a layer to share a pure function — that's a package. Layers earn
39
+ their weight when you're sharing *Nuxt-shaped* surface (auto-imported components,
40
+ pages, server handlers) and config.
@@ -160,3 +160,4 @@ const handleSave = async () => {
160
160
  3. **Pages are entry points** - Think of them as "controllers" that compose "views"
161
161
  4. **Middleware for auth** - Use `definePageMeta({ middleware: 'auth' })`, not inline checks
162
162
  5. **Layouts for shared UI** - Headers, footers, sidebars go in `/layouts`, not repeated in pages
163
+ 6. **Remount on query-only change** - Vue reuses the page component across `?tab=` / `?id=` changes (same route), so `onMounted`/setup won't re-run. Force a clean remount with `definePageMeta({ key: (route) => route.fullPath })` when a page must re-initialize on a query change.
@@ -0,0 +1,41 @@
1
+ # Route Rules
2
+
3
+ `routeRules` in `nuxt.config.ts` declaratively apply caching, headers, redirects,
4
+ proxying, and CORS per URL pattern — no handler code. Reach for them before
5
+ hand-rolling middleware to do what a rule does in one line.
6
+
7
+ ```typescript
8
+ // nuxt.config.ts
9
+ export default defineNuxtConfig({
10
+ routeRules: {
11
+ "/api/**": { cors: true }, // CORS on all API routes
12
+ "/api/realtime/**": { cache: false }, // never cache SSE/streaming
13
+ "/blog/**": { swr: 3600 }, // cache + revalidate hourly
14
+ "/docs/**": { prerender: true }, // render at build time
15
+ "/old-path": { redirect: "/new-path" }, // 301 (redirectCode to change)
16
+ "/external/**": { proxy: "https://upstream.example/**" },// reverse-proxy
17
+ "/admin/**": { headers: { "X-Robots-Tag": "noindex" } },
18
+ },
19
+ });
20
+ ```
21
+
22
+ ## Semantics
23
+
24
+ - **Specificity wins:** a more specific path overrides a general one; `/api/**`
25
+ rules are overridden by `/api/realtime/**`.
26
+ - **`swr: N`** = stale-while-revalidate for N seconds; **`cache: { maxAge: N }`**
27
+ for a plain TTL; **`cache: false`** disables inherited caching (use on
28
+ streaming/per-user routes that a broader rule would otherwise cache).
29
+ - **`isr`** (incremental static regen) is the same idea on supported platforms.
30
+ - Rules apply to both Nitro routes and rendered pages under the path.
31
+
32
+ ## When to use a rule vs a handler
33
+
34
+ | Want | Use |
35
+ |---|---|
36
+ | Cache/headers/redirect/proxy/CORS by path | `routeRules` |
37
+ | Per-key cache with invalidation, SWR on a function | `defineCachedFunction` — [caching.md](./caching.md) |
38
+ | Logic (auth, body inspection, dynamic redirect) | server middleware / handler — [server-runtime.md](./server-runtime.md) |
39
+
40
+ Don't write a middleware that just sets a static header or redirects a fixed path
41
+ — that's a route rule.
@@ -0,0 +1,97 @@
1
+ # Nitro Server Runtime (version, middleware, context, $fetch, ws)
2
+
3
+ Server-side mechanics that aren't endpoints or validation: which Nitro you're on,
4
+ how `server/middleware/` runs, reading the event without threading it, the
5
+ internal-`$fetch` auth trap, and the WebSocket option.
6
+
7
+ ## ⚠️ Version: Nitro follows Nuxt — you're on v2
8
+
9
+ Nitro's version is **set by Nuxt, not chosen by you**. Nuxt 4.x ships
10
+ `@nuxt/nitro-server`, which depends on `nitropack ^2.x` — you have no direct
11
+ `nitropack` dependency to bump. **`nitro.build` now documents Nitro v3**, a
12
+ separate in-progress major Nuxt has not adopted; copying its examples verbatim
13
+ gives you code that doesn't compile here. Ignore the v3 renames until Nuxt ships
14
+ on v3. Map back to v2:
15
+
16
+ | v3 docs (nitro.build) | v2 (this project) |
17
+ |---|---|
18
+ | `defineHandler` | `defineEventHandler` (auto-imported) |
19
+ | `definePlugin` | `defineNitroPlugin` |
20
+ | `import { defineCachedEventHandler } from "nitro/cache"` | auto-imported global |
21
+ | `features: { websocket: true }` | `experimental: { websocket: true }` |
22
+ | `routes/` directory | `server/` directory |
23
+ | `render:response` → `response` hook | `render:response` |
24
+
25
+ When unsure, trust the installed `node_modules/nitropack/dist/runtime/*.d.ts`
26
+ over the website.
27
+
28
+ ## Server middleware (`server/middleware/`)
29
+
30
+ Every request runs **all** files in `server/middleware/` (not route-scoped).
31
+ Three rules that bite:
32
+
33
+ - **Order is filename order** — prefix numerically: `1.logger.ts`, `2.auth.ts`.
34
+ - **Don't return a value.** Returning a body **ends the request** there. Set
35
+ `event.context.*` and return nothing.
36
+ - **Scope by checking `event.path`** yourself — there's no per-path registration.
37
+
38
+ ```typescript
39
+ // server/middleware/2.auth.ts — runs after 1.*, sets context, returns nothing
40
+ export default defineEventHandler((event) => {
41
+ if (!event.path.startsWith("/api/")) return;
42
+ event.context.user = parseUser(event); // ❌ a `return user` here would 200 every request
43
+ });
44
+ ```
45
+
46
+ ## `useEvent()` — read the event without threading it
47
+
48
+ With `experimental.asyncContext: true` (set in `nuxt.config.ts`), `useEvent()`
49
+ returns the current `H3Event` from async context, so deep util/service layers can
50
+ read the session/headers without every caller passing `event` down:
51
+
52
+ ```typescript
53
+ // nuxt.config.ts → nitro: { experimental: { asyncContext: true } }
54
+ import { useEvent } from "nitropack/runtime"; // auto-imported in server/
55
+
56
+ export async function currentUser() {
57
+ const event = useEvent(); // no event parameter needed
58
+ return (await getUserSession(event)).user;
59
+ }
60
+ ```
61
+
62
+ Node-runtime only; it's flagged experimental. Without the flag, `useEvent()`
63
+ throws — keep threading `event` explicitly.
64
+
65
+ ## Internal `$fetch` does NOT forward cookies
66
+
67
+ On the server, `$fetch("/api/…")` to an internal route short-circuits HTTP (direct
68
+ handler call — faster), but it **does not carry the incoming request's
69
+ cookies/headers**. A session-dependent internal call silently sees no user. Pass
70
+ them through:
71
+
72
+ ```typescript
73
+ // inside a handler, calling another internal route that needs the session:
74
+ await $fetch("/api/me", { headers: { cookie: getHeader(event, "cookie") ?? "" } });
75
+ ```
76
+
77
+ ## WebSockets — when SSE isn't enough
78
+
79
+ [sse.md](./sse.md) covers one-way streaming. For bidirectional (client → server),
80
+ Nitro has `defineWebSocketHandler` behind `experimental.websocket: true` (NOT v3's
81
+ `features.websocket`), with built-in pub/sub:
82
+
83
+ ```typescript
84
+ // server/routes/ws.ts
85
+ export default defineWebSocketHandler({
86
+ open(peer) { peer.subscribe("room"); },
87
+ message(peer, msg) { peer.publish("room", msg.text()); }, // publish excludes sender
88
+ close(peer) { /* cleanup */ },
89
+ });
90
+ ```
91
+
92
+ ## Two `useDatabase`s — don't confuse them
93
+
94
+ Nitro ships its own `useDatabase()` (a `db0`-backed SQL layer, gated on
95
+ `experimental.database`). This project **shadows** it with the Kysely
96
+ `useDatabase()` in `server/utils/db.ts` (see [composables-utils.md](./composables-utils.md)).
97
+ Use the Kysely one; don't enable `nitro.database` or import Nitro's.
@@ -113,6 +113,66 @@ With `@vueuse/nuxt`, these are auto-imported:
113
113
  - `useFetch` - use Nuxt's version
114
114
  - `useHead` - use Nuxt's version
115
115
 
116
+ ## `useCookie` / `useState` for SSR-shared state
117
+
118
+ `useLocalStorage` reads only on the client, so state that must be **correct in the
119
+ first server-rendered paint** (a theme, sidebar-collapsed flag, accounting-basis
120
+ toggle) flickers if stored in localStorage. Persist it in a cookie instead — the
121
+ server sees it and renders the right thing immediately:
122
+
123
+ ```typescript
124
+ const basis = useCookie<"accrual" | "cash">("basis", { default: () => "accrual" });
125
+ // readable during SSR; writes sync to the cookie. No onMounted flicker.
126
+ ```
127
+
128
+ For SSR-shared state that needn't persist across reloads, use `useState` (it
129
+ serializes the server value to the client, so both render identically). See
130
+ [state-management.md](./state-management.md) for the full `useState` story
131
+ (including the never-export-a-module-`ref` rule):
132
+
133
+ ```typescript
134
+ const expanded = useState("nav-expanded", () => false);
135
+ ```
136
+
137
+ `useState` is also the fix for **SSR-nondeterministic values** — anything from
138
+ `Math.random()` / `Date.now()` rendered into markup mismatches on hydration unless
139
+ the server's choice is serialized:
140
+
141
+ ```typescript
142
+ const variant = useState("hero-variant", () => Math.floor(Math.random() * 3)); // server picks once, client reuses
143
+ ```
144
+
145
+ ### `useCookie` options worth knowing
146
+
147
+ - **Nested writes need `watch: 'deep'`** — the default shallow watch won't
148
+ persist a mutation to a nested property of a cookie object.
149
+ - **`httpOnly` cookies are unreadable client-side** — reading one in setup yields
150
+ different values on server vs client → hydration mismatch.
151
+ - **`refreshCookie(name)`** re-syncs the ref if the cookie changed out-of-band
152
+ (e.g. set by an API response).
153
+ - 4 KB size limit (see gotcha below).
154
+
155
+ ## SSR-safe URL & request headers
156
+
157
+ `window.location` and request headers don't exist during SSR. Don't guard them
158
+ away — use the universal composables that work on both sides:
159
+
160
+ - **`useRequestURL()`** → a `URL` object available on server *and* client. Use it
161
+ for `origin`/`hostname`/`searchParams` instead of `window.location`:
162
+
163
+ ```typescript
164
+ const url = useRequestURL();
165
+ const origin = url.origin; // works during SSR; window.location would throw
166
+ ```
167
+
168
+ - **`useRequestHeaders(['cookie'])`** → inbound request headers on the server
169
+ (`{}` on the client). The canonical way to **forward auth** when an SSR
170
+ `useFetch` calls an endpoint that needs the session cookie:
171
+
172
+ ```typescript
173
+ const { data } = await useFetch("/api/me", { headers: useRequestHeaders(["cookie"]) });
174
+ ```
175
+
116
176
  ## Hydration Mismatch Prevention
117
177
 
118
178
  **Problem:** Server renders with default, client reads different value = mismatch.