create-magic-storefront 0.1.1 → 0.1.2
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/package.json +1 -1
- package/template/.claude/skills/storefront-design/SKILL.md +10 -3
- package/template/.claude/skills/storefront-verify/SKILL.md +85 -0
- package/template/.claude/skills/vercel-react-best-practices/AGENTS.md +3810 -0
- package/template/.claude/skills/vercel-react-best-practices/README.md +123 -0
- package/template/.claude/skills/vercel-react-best-practices/SKILL.md +149 -0
- package/template/.claude/skills/vercel-react-best-practices/SOURCE.md +7 -0
- package/template/.claude/skills/vercel-react-best-practices/metadata.json +15 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/_sections.md +46 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/_template.md +28 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md +56 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md +82 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md +63 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +60 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-request-idle-callback.md +105 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md +59 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +149 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md +50 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/template/.claude/skills/web-interface-guidelines/LICENSE +21 -0
- package/template/.claude/skills/web-interface-guidelines/SKILL.md +40 -0
- package/template/.claude/skills/web-interface-guidelines/SOURCE.md +9 -0
- package/template/.claude/skills/web-interface-guidelines/guidelines.md +155 -0
- package/template/AGENTS.md +7 -0
- package/template/PAGES.md +7 -2
- package/template/_gitignore +1 -0
- package/template/app/account/page.tsx +31 -11
- package/template/app/cart/page.tsx +50 -54
- package/template/app/checkout/page.tsx +86 -38
- package/template/app/collections/[handle]/page.tsx +3 -2
- package/template/app/error.tsx +6 -4
- package/template/app/globals.css +29 -0
- package/template/app/layout.tsx +4 -2
- package/template/app/not-found.tsx +5 -2
- package/template/app/pages/[handle]/page.tsx +3 -2
- package/template/app/search/page.tsx +11 -6
- package/template/components/buy-box.tsx +7 -4
- package/template/components/cart-link.tsx +8 -2
- package/template/components/pager.tsx +3 -1
- package/template/components/product-grid.tsx +9 -5
- package/template/components/sections/banner.tsx +7 -1
- package/template/components/sections/deal-of-day.tsx +16 -5
- package/template/components/sections/product-shelves.tsx +8 -7
- package/template/components/sections/store-reviews.tsx +7 -5
- package/template/lib/errors.ts +1 -4
- package/template/lib/i18n.ts +315 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Avoid Duplicate Serialization in RSC Props
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: reduces network payload by avoiding duplicate serialization
|
|
5
|
+
tags: server, rsc, serialization, props, client-components
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Avoid Duplicate Serialization in RSC Props
|
|
9
|
+
|
|
10
|
+
**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
|
|
11
|
+
|
|
12
|
+
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
|
|
13
|
+
|
|
14
|
+
**Incorrect (duplicates array):**
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
// RSC: sends 6 strings (2 arrays × 3 items)
|
|
18
|
+
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Correct (sends 3 strings):**
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// RSC: send once
|
|
25
|
+
<ClientList usernames={usernames} />
|
|
26
|
+
|
|
27
|
+
// Client: transform there
|
|
28
|
+
'use client'
|
|
29
|
+
const sorted = useMemo(() => [...usernames].sort(), [usernames])
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Nested deduplication behavior:**
|
|
33
|
+
|
|
34
|
+
Deduplication works recursively. Impact varies by data type:
|
|
35
|
+
|
|
36
|
+
- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
|
|
37
|
+
- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// string[] - duplicates everything
|
|
41
|
+
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
|
|
42
|
+
|
|
43
|
+
// object[] - duplicates array structure only
|
|
44
|
+
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Operations breaking deduplication (create new references):**
|
|
48
|
+
|
|
49
|
+
- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
|
|
50
|
+
- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
|
|
51
|
+
|
|
52
|
+
**More examples:**
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// ❌ Bad
|
|
56
|
+
<C users={users} active={users.filter(u => u.active)} />
|
|
57
|
+
<C product={product} productName={product.name} />
|
|
58
|
+
|
|
59
|
+
// ✅ Good
|
|
60
|
+
<C users={users} />
|
|
61
|
+
<C product={product} />
|
|
62
|
+
// Do filtering/destructuring in client
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Hoist Static I/O to Module Level
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: avoids repeated file/network I/O per request
|
|
5
|
+
tags: server, io, performance, next.js, route-handlers, og-image
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Hoist Static I/O to Module Level
|
|
9
|
+
|
|
10
|
+
**Impact: HIGH (avoids repeated file/network I/O per request)**
|
|
11
|
+
|
|
12
|
+
When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.
|
|
13
|
+
|
|
14
|
+
**Incorrect (reads font file on every request):**
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// app/api/og/route.tsx
|
|
18
|
+
import { ImageResponse } from 'next/og'
|
|
19
|
+
|
|
20
|
+
export async function GET(request: Request) {
|
|
21
|
+
// Runs on EVERY request - expensive!
|
|
22
|
+
const fontData = await fetch(
|
|
23
|
+
new URL('./fonts/Inter.ttf', import.meta.url)
|
|
24
|
+
).then(res => res.arrayBuffer())
|
|
25
|
+
|
|
26
|
+
const logoData = await fetch(
|
|
27
|
+
new URL('./images/logo.png', import.meta.url)
|
|
28
|
+
).then(res => res.arrayBuffer())
|
|
29
|
+
|
|
30
|
+
return new ImageResponse(
|
|
31
|
+
<div style={{ fontFamily: 'Inter' }}>
|
|
32
|
+
<img src={logoData} />
|
|
33
|
+
Hello World
|
|
34
|
+
</div>,
|
|
35
|
+
{ fonts: [{ name: 'Inter', data: fontData }] }
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Correct (loads once at module initialization):**
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// app/api/og/route.tsx
|
|
44
|
+
import { ImageResponse } from 'next/og'
|
|
45
|
+
|
|
46
|
+
// Module-level: runs ONCE when module is first imported
|
|
47
|
+
const fontData = fetch(
|
|
48
|
+
new URL('./fonts/Inter.ttf', import.meta.url)
|
|
49
|
+
).then(res => res.arrayBuffer())
|
|
50
|
+
|
|
51
|
+
const logoData = fetch(
|
|
52
|
+
new URL('./images/logo.png', import.meta.url)
|
|
53
|
+
).then(res => res.arrayBuffer())
|
|
54
|
+
|
|
55
|
+
export async function GET(request: Request) {
|
|
56
|
+
// Await the already-started promises
|
|
57
|
+
const [font, logo] = await Promise.all([fontData, logoData])
|
|
58
|
+
|
|
59
|
+
return new ImageResponse(
|
|
60
|
+
<div style={{ fontFamily: 'Inter' }}>
|
|
61
|
+
<img src={logo} />
|
|
62
|
+
Hello World
|
|
63
|
+
</div>,
|
|
64
|
+
{ fonts: [{ name: 'Inter', data: font }] }
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Correct (synchronous fs at module level):**
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// app/api/og/route.tsx
|
|
73
|
+
import { ImageResponse } from 'next/og'
|
|
74
|
+
import { readFileSync } from 'fs'
|
|
75
|
+
import { join } from 'path'
|
|
76
|
+
|
|
77
|
+
// Synchronous read at module level - blocks only during module init
|
|
78
|
+
const fontData = readFileSync(
|
|
79
|
+
join(process.cwd(), 'public/fonts/Inter.ttf')
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const logoData = readFileSync(
|
|
83
|
+
join(process.cwd(), 'public/images/logo.png')
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
export async function GET(request: Request) {
|
|
87
|
+
return new ImageResponse(
|
|
88
|
+
<div style={{ fontFamily: 'Inter' }}>
|
|
89
|
+
<img src={logoData} />
|
|
90
|
+
Hello World
|
|
91
|
+
</div>,
|
|
92
|
+
{ fonts: [{ name: 'Inter', data: fontData }] }
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Incorrect (reads config on every call):**
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import fs from 'node:fs/promises'
|
|
101
|
+
|
|
102
|
+
export async function processRequest(data: Data) {
|
|
103
|
+
const config = JSON.parse(
|
|
104
|
+
await fs.readFile('./config.json', 'utf-8')
|
|
105
|
+
)
|
|
106
|
+
const template = await fs.readFile('./template.html', 'utf-8')
|
|
107
|
+
|
|
108
|
+
return render(template, data, config)
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Correct (hoists config and template to module level):**
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import fs from 'node:fs/promises'
|
|
116
|
+
|
|
117
|
+
const configPromise = fs
|
|
118
|
+
.readFile('./config.json', 'utf-8')
|
|
119
|
+
.then(JSON.parse)
|
|
120
|
+
const templatePromise = fs.readFile('./template.html', 'utf-8')
|
|
121
|
+
|
|
122
|
+
export async function processRequest(data: Data) {
|
|
123
|
+
const [config, template] = await Promise.all([
|
|
124
|
+
configPromise,
|
|
125
|
+
templatePromise,
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
return render(template, data, config)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
When to use this pattern:
|
|
133
|
+
|
|
134
|
+
- Loading fonts for OG image generation
|
|
135
|
+
- Loading static logos, icons, or watermarks
|
|
136
|
+
- Reading configuration files that don't change at runtime
|
|
137
|
+
- Loading email templates or other static templates
|
|
138
|
+
- Any static asset that's the same across all requests
|
|
139
|
+
|
|
140
|
+
When not to use this pattern:
|
|
141
|
+
|
|
142
|
+
- Assets that vary per request or user
|
|
143
|
+
- Files that may change during runtime (use caching with TTL instead)
|
|
144
|
+
- Large files that would consume too much memory if kept loaded
|
|
145
|
+
- Sensitive data that shouldn't persist in memory
|
|
146
|
+
|
|
147
|
+
With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute), module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.
|
|
148
|
+
|
|
149
|
+
In traditional serverless, each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
|
package/template/.claude/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Avoid Shared Module State for Request Data
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents concurrency bugs and request data leaks
|
|
5
|
+
tags: server, rsc, ssr, concurrency, security, state
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Avoid Shared Module State for Request Data
|
|
9
|
+
|
|
10
|
+
For React Server Components and client components rendered during SSR, avoid using mutable module-level variables to share request-scoped data. Server renders can run concurrently in the same process. If one render writes to shared module state and another render reads it, you can get race conditions, cross-request contamination, and security bugs where one user's data appears in another user's response.
|
|
11
|
+
|
|
12
|
+
Treat module scope on the server as process-wide shared memory, not request-local state.
|
|
13
|
+
|
|
14
|
+
**Incorrect (request data leaks across concurrent renders):**
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
let currentUser: User | null = null
|
|
18
|
+
|
|
19
|
+
export default async function Page() {
|
|
20
|
+
currentUser = await auth()
|
|
21
|
+
return <Dashboard />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function Dashboard() {
|
|
25
|
+
return <div>{currentUser?.name}</div>
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If two requests overlap, request A can set `currentUser`, then request B overwrites it before request A finishes rendering `Dashboard`.
|
|
30
|
+
|
|
31
|
+
**Correct (keep request data local to the render tree):**
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
export default async function Page() {
|
|
35
|
+
const user = await auth()
|
|
36
|
+
return <Dashboard user={user} />
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Dashboard({ user }: { user: User | null }) {
|
|
40
|
+
return <div>{user?.name}</div>
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Safe exceptions:
|
|
45
|
+
|
|
46
|
+
- Immutable static assets or config loaded once at module scope
|
|
47
|
+
- Shared caches intentionally designed for cross-request reuse and keyed correctly
|
|
48
|
+
- Process-wide singletons that do not store request- or user-specific mutable data
|
|
49
|
+
|
|
50
|
+
For static assets and config, see [Hoist Static I/O to Module Level](./server-hoist-static-io.md).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Parallel Data Fetching with Component Composition
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: eliminates server-side waterfalls
|
|
5
|
+
tags: server, rsc, parallel-fetching, composition
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Parallel Data Fetching with Component Composition
|
|
9
|
+
|
|
10
|
+
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
|
|
11
|
+
|
|
12
|
+
**Incorrect (Sidebar waits for Page's fetch to complete):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
export default async function Page() {
|
|
16
|
+
const header = await fetchHeader()
|
|
17
|
+
return (
|
|
18
|
+
<div>
|
|
19
|
+
<div>{header}</div>
|
|
20
|
+
<Sidebar />
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function Sidebar() {
|
|
26
|
+
const items = await fetchSidebarItems()
|
|
27
|
+
return <nav>{items.map(renderItem)}</nav>
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Correct (both fetch simultaneously):**
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
async function Header() {
|
|
35
|
+
const data = await fetchHeader()
|
|
36
|
+
return <div>{data}</div>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function Sidebar() {
|
|
40
|
+
const items = await fetchSidebarItems()
|
|
41
|
+
return <nav>{items.map(renderItem)}</nav>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function Page() {
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<Header />
|
|
48
|
+
<Sidebar />
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Alternative with children prop:**
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
async function Header() {
|
|
58
|
+
const data = await fetchHeader()
|
|
59
|
+
return <div>{data}</div>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function Sidebar() {
|
|
63
|
+
const items = await fetchSidebarItems()
|
|
64
|
+
return <nav>{items.map(renderItem)}</nav>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Layout({ children }: { children: ReactNode }) {
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
<Header />
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function Page() {
|
|
77
|
+
return (
|
|
78
|
+
<Layout>
|
|
79
|
+
<Sidebar />
|
|
80
|
+
</Layout>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
```
|
package/template/.claude/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Parallel Nested Data Fetching
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: eliminates server-side waterfalls
|
|
5
|
+
tags: server, rsc, parallel-fetching, promise-chaining
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Parallel Nested Data Fetching
|
|
9
|
+
|
|
10
|
+
When fetching nested data in parallel, chain dependent fetches within each item's promise so a slow item doesn't block the rest.
|
|
11
|
+
|
|
12
|
+
**Incorrect (a single slow item blocks all nested fetches):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
const chats = await Promise.all(
|
|
16
|
+
chatIds.map(id => getChat(id))
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const chatAuthors = await Promise.all(
|
|
20
|
+
chats.map(chat => getUser(chat.author))
|
|
21
|
+
)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If one `getChat(id)` out of 100 is extremely slow, the authors of the other 99 chats can't start loading even though their data is ready.
|
|
25
|
+
|
|
26
|
+
**Correct (each item chains its own nested fetch):**
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
const chatAuthors = await Promise.all(
|
|
30
|
+
chatIds.map(id => getChat(id).then(chat => getUser(chat.author)))
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Each item independently chains `getChat` → `getUser`, so a slow chat doesn't block author fetches for the others.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Minimize Serialization at RSC Boundaries
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: reduces data transfer size
|
|
5
|
+
tags: server, rsc, serialization, props
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Minimize Serialization at RSC Boundaries
|
|
9
|
+
|
|
10
|
+
The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
|
|
11
|
+
|
|
12
|
+
**Incorrect (serializes all 50 fields):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
async function Page() {
|
|
16
|
+
const user = await fetchUser() // 50 fields
|
|
17
|
+
return <Profile user={user} />
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
'use client'
|
|
21
|
+
function Profile({ user }: { user: User }) {
|
|
22
|
+
return <div>{user.name}</div> // uses 1 field
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Correct (serializes only 1 field):**
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
async function Page() {
|
|
30
|
+
const user = await fetchUser()
|
|
31
|
+
return <Profile name={user.name} />
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
'use client'
|
|
35
|
+
function Profile({ name }: { name: string }) {
|
|
36
|
+
return <div>{name}</div>
|
|
37
|
+
}
|
|
38
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Vercel Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-interface-guidelines
|
|
3
|
+
description: >-
|
|
4
|
+
Vercel's Web Interface Guidelines: keyboard and focus, forms, touch targets, animation, layout,
|
|
5
|
+
content, performance and contrast rules for accessible, polished UIs. Use when building or
|
|
6
|
+
reviewing any interactive UI in this storefront (header, product options, cart, checkout,
|
|
7
|
+
account forms, dialogs, menus), when asked to "review the UI", "check accessibility" or "audit
|
|
8
|
+
UX", and as the review pass of `storefront-verify`.
|
|
9
|
+
license: MIT
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Web Interface Guidelines
|
|
13
|
+
|
|
14
|
+
The rules are in [`guidelines.md`](guidelines.md) (MUST / SHOULD / NEVER), vendored from Vercel so
|
|
15
|
+
they work offline. Read it before building or reviewing interactive UI.
|
|
16
|
+
|
|
17
|
+
## Building
|
|
18
|
+
|
|
19
|
+
Apply the MUST rules as you write. The ones a storefront breaks most often:
|
|
20
|
+
|
|
21
|
+
- Forms (checkout, account, OTP): a `<label>` for every field, the right `type`, `inputmode` and
|
|
22
|
+
`autocomplete` (`tel`, `one-time-code`, `street-address`), errors next to the field, focus on the
|
|
23
|
+
first error on submit, the submit button enabled until the request starts.
|
|
24
|
+
- Targets: at least 44px on mobile (the storefront also runs as a Telegram Mini App), mobile
|
|
25
|
+
`<input>` font-size at least 16px.
|
|
26
|
+
- Focus: a visible `:focus-visible` ring on every control; a sticky header never covers it.
|
|
27
|
+
- Feedback: cart and checkout updates announced with `aria-live="polite"`.
|
|
28
|
+
- Images: `alt` from the API or the product title; decorative images `alt=""`.
|
|
29
|
+
|
|
30
|
+
Where `storefront-design` and this file disagree, `storefront-design` wins (it knows the API: for
|
|
31
|
+
example, contrast of a color pair the merchant set is reported, not overridden).
|
|
32
|
+
|
|
33
|
+
## Reviewing
|
|
34
|
+
|
|
35
|
+
1. Read `guidelines.md`.
|
|
36
|
+
2. Read the files under review (or every file changed since the last commit:
|
|
37
|
+
`git diff --name-only HEAD`).
|
|
38
|
+
3. Report every violation as `path:line — rule — fix`, most severe first (MUST before SHOULD).
|
|
39
|
+
No findings: say so in one line.
|
|
40
|
+
4. Fix the MUST violations unless asked only to review.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Source
|
|
2
|
+
|
|
3
|
+
`guidelines.md` is vendored unchanged from
|
|
4
|
+
[vercel-labs/web-interface-guidelines](https://github.com/vercel-labs/web-interface-guidelines),
|
|
5
|
+
`AGENTS.md` at commit `e3d624baaf29dc1fc645aff3e38f03e564d2d6b1` (MIT, see `LICENSE`). `SKILL.md`
|
|
6
|
+
is this storefront's own: it tells the agent when to apply the guidelines, and works offline
|
|
7
|
+
instead of fetching them.
|
|
8
|
+
|
|
9
|
+
To update, copy the upstream `AGENTS.md` over `guidelines.md` and record the new commit above.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Concise rules for building accessible, fast, delightful UIs. Use MUST/SHOULD/NEVER to guide decisions.
|
|
2
|
+
|
|
3
|
+
## Interactions
|
|
4
|
+
|
|
5
|
+
### Keyboard
|
|
6
|
+
|
|
7
|
+
- MUST: Full keyboard support per [WAI-ARIA APG](https://www.w3.org/WAI/ARIA/apg/patterns/)
|
|
8
|
+
- MUST: Visible, unobscured focus rings (`:focus-visible`; group with `:focus-within`); sticky/fixed elements never cover focus
|
|
9
|
+
- MUST: Manage focus (trap, move, return) per APG patterns
|
|
10
|
+
- NEVER: `outline: none` without visible focus replacement
|
|
11
|
+
|
|
12
|
+
### Targets & Input
|
|
13
|
+
|
|
14
|
+
- MUST: Hit target ≥24px (mobile ≥44px); if visual <24px, expand hit area
|
|
15
|
+
- MUST: Mobile `<input>` font-size ≥16px to prevent iOS zoom
|
|
16
|
+
- NEVER: Disable browser zoom (`user-scalable=no`, `maximum-scale=1`)
|
|
17
|
+
- MUST: `touch-action: manipulation` to prevent double-tap zoom
|
|
18
|
+
- SHOULD: Set `-webkit-tap-highlight-color` to match design
|
|
19
|
+
|
|
20
|
+
### Forms
|
|
21
|
+
|
|
22
|
+
- MUST: Hydration-safe inputs (no lost focus/value)
|
|
23
|
+
- NEVER: Block paste in `<input>`/`<textarea>`
|
|
24
|
+
- MUST: Loading buttons show spinner and keep original label
|
|
25
|
+
- MUST: Enter submits focused input; in `<textarea>`, ⌘/Ctrl+Enter submits
|
|
26
|
+
- MUST: Keep submit enabled until request starts; then disable with spinner
|
|
27
|
+
- MUST: Accept free text, validate after—don't block typing
|
|
28
|
+
- MUST: Allow incomplete form submission to surface validation
|
|
29
|
+
- MUST: Errors inline next to fields; on submit, focus first error
|
|
30
|
+
- MUST: `autocomplete` + meaningful `name`; correct `type` and `inputmode`
|
|
31
|
+
- SHOULD: Disable spellcheck for emails/codes/usernames
|
|
32
|
+
- SHOULD: Placeholders end with `…` and show example pattern
|
|
33
|
+
- MUST: Warn on unsaved changes before navigation
|
|
34
|
+
- MUST: Compatible with password managers & 2FA; allow pasting codes
|
|
35
|
+
- MUST: Trim values to handle text expansion trailing spaces
|
|
36
|
+
- MUST: No dead zones on checkboxes/radios; label+control share one hit target
|
|
37
|
+
|
|
38
|
+
### State & Navigation
|
|
39
|
+
|
|
40
|
+
- MUST: URL reflects state (deep-link filters/tabs/pagination/expanded panels)
|
|
41
|
+
- MUST: Back/Forward restores scroll position
|
|
42
|
+
- MUST: Links use `<a>`/`<Link>` for navigation (support Cmd/Ctrl/middle-click)
|
|
43
|
+
- NEVER: Use `<div onClick>` for navigation
|
|
44
|
+
|
|
45
|
+
### Feedback
|
|
46
|
+
|
|
47
|
+
- SHOULD: Optimistic UI; reconcile on response; on failure rollback or offer Undo
|
|
48
|
+
- MUST: Confirm destructive actions or provide Undo window
|
|
49
|
+
- MUST: Use polite `aria-live` for toasts/inline validation
|
|
50
|
+
- SHOULD: Ellipsis (`…`) for options opening follow-ups ("Rename…") and loading states ("Loading…")
|
|
51
|
+
|
|
52
|
+
### Touch & Drag
|
|
53
|
+
|
|
54
|
+
- MUST: Generous targets, clear affordances; avoid finicky interactions
|
|
55
|
+
- MUST: Delay first tooltip; subsequent peers instant
|
|
56
|
+
- MUST: `overscroll-behavior: contain` in modals/drawers
|
|
57
|
+
- MUST: During drag, disable text selection and set `inert` on dragged elements
|
|
58
|
+
- MUST: Drag/swipe/pinch/path gestures have a tap/click and keyboard alternative unless essential
|
|
59
|
+
- MUST: If it looks clickable, it must be clickable
|
|
60
|
+
|
|
61
|
+
### Autofocus
|
|
62
|
+
|
|
63
|
+
- SHOULD: Autofocus on desktop with single primary input; rarely on mobile
|
|
64
|
+
|
|
65
|
+
## Animation
|
|
66
|
+
|
|
67
|
+
- MUST: Honor `prefers-reduced-motion` (provide reduced variant or disable)
|
|
68
|
+
- SHOULD: Prefer CSS > Web Animations API > JS libraries
|
|
69
|
+
- MUST: Animate compositor-friendly props (`transform`, `opacity`) only
|
|
70
|
+
- NEVER: Animate layout props (`top`, `left`, `width`, `height`)
|
|
71
|
+
- NEVER: `transition: all`—list properties explicitly
|
|
72
|
+
- SHOULD: Animate only to clarify cause/effect or add deliberate delight
|
|
73
|
+
- SHOULD: Choose easing to match the change (size/distance/trigger)
|
|
74
|
+
- MUST: Animations interruptible and input-driven; autoplay only for muted, non-essential loops
|
|
75
|
+
- MUST: Autoplay motion >5s alongside other content has pause, stop, or hide controls
|
|
76
|
+
- MUST: Correct `transform-origin` (motion starts where it "physically" should)
|
|
77
|
+
- MUST: SVG transforms on `<g>` wrapper with `transform-box: fill-box`
|
|
78
|
+
|
|
79
|
+
## Layout
|
|
80
|
+
|
|
81
|
+
- SHOULD: Optical alignment; adjust ±1px when perception beats geometry
|
|
82
|
+
- MUST: Deliberate alignment to grid/baseline/edges—no accidental placement
|
|
83
|
+
- SHOULD: Balance icon/text lockups (weight/size/spacing/color)
|
|
84
|
+
- MUST: Verify mobile, laptop, ultra-wide (simulate ultra-wide at 50% zoom)
|
|
85
|
+
- MUST: Respect safe areas (`env(safe-area-inset-*)`)
|
|
86
|
+
- MUST: Avoid unwanted scrollbars; fix overflows
|
|
87
|
+
- SHOULD: Flex/grid over JS measurement for layout
|
|
88
|
+
|
|
89
|
+
## Content & Accessibility
|
|
90
|
+
|
|
91
|
+
- SHOULD: Inline help first; tooltips last resort
|
|
92
|
+
- MUST: Skeletons mirror final content to avoid layout shift
|
|
93
|
+
- MUST: `<title>` matches current context
|
|
94
|
+
- MUST: No dead ends; always offer next step/recovery
|
|
95
|
+
- MUST: Design empty/sparse/dense/error states
|
|
96
|
+
- SHOULD: Curly quotes (“ ”); avoid widows/orphans (`text-wrap: balance`)
|
|
97
|
+
- MUST: `font-variant-numeric: tabular-nums` for number comparisons
|
|
98
|
+
- MUST: Redundant status cues (not color-only); icons have text labels
|
|
99
|
+
- MUST: Accessible names exist even when visuals omit labels
|
|
100
|
+
- MUST: Use `…` character (not `...`)
|
|
101
|
+
- MUST: `scroll-margin-top` on headings; "Skip to content" link; hierarchical `<h1>`–`<h6>`
|
|
102
|
+
- MUST: Resilient to user-generated content (short/avg/very long)
|
|
103
|
+
- MUST: Locale-aware dates/times/numbers (`Intl.DateTimeFormat`, `Intl.NumberFormat`)
|
|
104
|
+
- SHOULD: `translate="no"` on brand names, code tokens, & identifiers to prevent garbled auto-translation
|
|
105
|
+
- MUST: Accurate `aria-label`; decorative elements `aria-hidden`
|
|
106
|
+
- MUST: Icon-only buttons have descriptive `aria-label`
|
|
107
|
+
- MUST: Prefer native semantics (`button`, `a`, `label`, `table`) before ARIA
|
|
108
|
+
- MUST: Media has captions/transcripts/descriptions as applicable; controls are keyboard-operable; decorative media hidden from assistive tech
|
|
109
|
+
- MUST: Non-breaking spaces: `10 MB`, `⌘ K`, brand names
|
|
110
|
+
|
|
111
|
+
## Content Handling
|
|
112
|
+
|
|
113
|
+
- MUST: Text containers handle long content (`truncate`, `line-clamp-*`, `break-words`)
|
|
114
|
+
- MUST: Flex children need `min-w-0` to allow truncation
|
|
115
|
+
- MUST: Handle empty states—no broken UI for empty strings/arrays
|
|
116
|
+
|
|
117
|
+
## Performance
|
|
118
|
+
|
|
119
|
+
- SHOULD: Test iOS Low Power Mode and macOS Safari
|
|
120
|
+
- MUST: Measure reliably (disable extensions that skew runtime)
|
|
121
|
+
- MUST: Track and minimize re-renders (React DevTools/React Scan)
|
|
122
|
+
- MUST: Profile with CPU/network throttling
|
|
123
|
+
- MUST: Batch layout reads/writes; avoid reflows/repaints
|
|
124
|
+
- MUST: Mutations (`POST`/`PATCH`/`DELETE`) target <500ms
|
|
125
|
+
- SHOULD: Prefer uncontrolled inputs; controlled inputs cheap per keystroke
|
|
126
|
+
- MUST: Virtualize large lists (>50 items)
|
|
127
|
+
- MUST: Preload above-fold images; lazy-load the rest
|
|
128
|
+
- MUST: Prevent CLS (explicit image dimensions)
|
|
129
|
+
- SHOULD: `<link rel="preconnect">` for CDN domains
|
|
130
|
+
- SHOULD: Critical fonts: `<link rel="preload" as="font">` with `font-display: swap`
|
|
131
|
+
- SHOULD: `<video autoplay muted loop playsinline>` over animated GIF; provide still/reduced-motion alternative
|
|
132
|
+
- SHOULD: Short non-essential loops include a Safari H.264 MP4 `<picture>` source, `prefers-reduced-motion` media condition, and still fallback
|
|
133
|
+
|
|
134
|
+
## Dark Mode & Theming
|
|
135
|
+
|
|
136
|
+
- MUST: `color-scheme: dark` on `<html>` for dark themes
|
|
137
|
+
- SHOULD: `<meta name="theme-color">` matches page background
|
|
138
|
+
- MUST: Native `<select>`: explicit `background-color` and `color` (Windows fix)
|
|
139
|
+
|
|
140
|
+
## Hydration
|
|
141
|
+
|
|
142
|
+
- MUST: Inputs with `value` need `onChange` (or use `defaultValue`)
|
|
143
|
+
- SHOULD: Guard date/time rendering against hydration mismatch
|
|
144
|
+
|
|
145
|
+
## Design
|
|
146
|
+
|
|
147
|
+
- SHOULD: Layered shadows (ambient + direct)
|
|
148
|
+
- SHOULD: Crisp edges via semi-transparent borders + shadows
|
|
149
|
+
- SHOULD: Nested radii: child ≤ parent; concentric
|
|
150
|
+
- SHOULD: Hue consistency: tint borders/shadows/text toward bg hue
|
|
151
|
+
- MUST: Accessible charts (color-blind-friendly palettes)
|
|
152
|
+
- MUST: Meet contrast—prefer [APCA](https://apcacontrast.com/) over WCAG 2
|
|
153
|
+
- MUST: Increase contrast on `:hover`/`:active`/`:focus`
|
|
154
|
+
- SHOULD: Match browser UI to bg
|
|
155
|
+
- SHOULD: Avoid dark color gradient banding (use background images when needed)
|
package/template/AGENTS.md
CHANGED
|
@@ -28,6 +28,8 @@ each error code means — then `PAGES.md` for how each page type is built, and
|
|
|
28
28
|
webhook). Anything personal — cart, customer, wishlist — lives in client components under
|
|
29
29
|
`MagicStoreProvider` (`app/providers.tsx`).
|
|
30
30
|
- Branch on `MagicStoreError.code`, show `error.detail`.
|
|
31
|
+
- No hard-coded interface text: every word the storefront shows comes from `lib/i18n.ts`, in
|
|
32
|
+
every language it has (`ru`, `uz`, `en`).
|
|
31
33
|
- Every page handles its empty, error and not-found states (`orNotFound` turns `NOT_FOUND` into
|
|
32
34
|
the 404 page).
|
|
33
35
|
|
|
@@ -40,6 +42,11 @@ from a landing page: brand colors come from `shop.branding` at runtime, product
|
|
|
40
42
|
come only from the API, nothing is invented (prices, badges, reviews, urgency), and the cart and
|
|
41
43
|
checkout stay conventional.
|
|
42
44
|
|
|
45
|
+
Before calling visual work done, follow `.claude/skills/storefront-verify/SKILL.md`: build, run,
|
|
46
|
+
screenshot every page type at 390 and 1280px, fix what you see. Interactive UI follows
|
|
47
|
+
`.claude/skills/web-interface-guidelines/SKILL.md`; React and Next.js code follows
|
|
48
|
+
`.claude/skills/vercel-react-best-practices/SKILL.md`.
|
|
49
|
+
|
|
43
50
|
## Where things go
|
|
44
51
|
|
|
45
52
|
| Path | What |
|