@gallopsystems/agent-skills 1.4.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.
- package/README.md +3 -1
- package/package.json +1 -1
- package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +1 -1
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +23 -8
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +1 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/caching.md +68 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +31 -1
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/error-handling.md +74 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +18 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/formatters.md +11 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/layers.md +40 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +1 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/route-rules.md +41 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-runtime.md +97 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +60 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/state-management.md +68 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/storage.md +59 -0
- package/plugins/volt-primevue/.claude-plugin/plugin.json +8 -0
- package/plugins/volt-primevue/skills/volt-primevue/SKILL.md +170 -0
- package/plugins/volt-primevue/skills/volt-primevue/config.md +51 -0
- package/plugins/volt-primevue/skills/volt-primevue/gotchas.md +115 -0
- package/plugins/volt-primevue/skills/volt-primevue/theming.md +137 -0
- package/plugins/vue-nuxt/.claude-plugin/plugin.json +8 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/SKILL.md +48 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/auto-imports.md +48 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/component-authoring.md +159 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/composables.md +95 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/reactivity.md +133 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/slots.md +139 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/template-idioms.md +142 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/v-model.md +106 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/watch.md +194 -0
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ Then install the skills you want:
|
|
|
18
18
|
/plugin install doctl@gallop-systems-agent-skills
|
|
19
19
|
/plugin install git-github@gallop-systems-agent-skills
|
|
20
20
|
/plugin install copier-template@gallop-systems-agent-skills
|
|
21
|
+
/plugin install volt-primevue@gallop-systems-agent-skills
|
|
22
|
+
/plugin install vue-nuxt@gallop-systems-agent-skills
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
## Updating
|
|
@@ -86,7 +88,7 @@ Covers:
|
|
|
86
88
|
|
|
87
89
|
### nuxt-nitro-api
|
|
88
90
|
|
|
89
|
-
Nuxt
|
|
91
|
+
Nuxt 4 / Nitro API patterns for building type-safe full-stack applications. Automatically activates when working in Nuxt 4 projects.
|
|
90
92
|
|
|
91
93
|
Covers:
|
|
92
94
|
- Zod validation with h3 (Standard Schema support)
|
package/package.json
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nuxt-nitro-api
|
|
3
|
-
description: Build type-safe Nuxt
|
|
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
|
|
6
|
+
# Nuxt 4 / Nitro API Patterns
|
|
7
7
|
|
|
8
|
-
This skill provides patterns for building type-safe Nuxt
|
|
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
|
|
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) -
|
|
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.
|