Initial commit

This commit is contained in:
2026-03-07 11:07:45 -03:00
commit 9d523f8b6a
65 changed files with 17311 additions and 0 deletions

View File

@@ -0,0 +1 @@
../../.agents/skills/building-admin-dashboard-customizations

View File

@@ -0,0 +1 @@
../../.agents/skills/building-storefronts

View File

@@ -0,0 +1 @@
../../.agents/skills/building-with-medusa

1
.agent/skills/db-generate Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/db-generate

1
.agent/skills/db-migrate Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/db-migrate

1
.agent/skills/new-user Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/new-user

View File

@@ -0,0 +1 @@
../../.agents/skills/storefront-best-practices

View File

@@ -0,0 +1,462 @@
---
name: building-admin-dashboard-customizations
description: Load automatically when planning, researching, or implementing Medusa Admin dashboard UI (widgets, custom pages, forms, tables, data loading, navigation). REQUIRED for all admin UI work in ALL modes (planning, implementation, exploration). Contains design patterns, component usage, and data loading patterns that MCP servers don't provide.
---
# Medusa Admin Dashboard Customizations
Build custom UI extensions for the Medusa Admin dashboard using the Admin SDK and Medusa UI components.
**Note:** "UI Routes" are custom admin pages, different from backend API routes (which use building-with-medusa skill).
## When to Apply
**Load this skill for ANY admin UI development task, including:**
- Creating widgets for product/order/customer pages
- Building custom admin pages
- Implementing forms and modals
- Displaying data with tables or lists
- Adding navigation between pages
**Also load these skills when:**
- **building-with-medusa:** Building backend API routes that the admin UI calls
- **building-storefronts:** If working on storefront instead of admin dashboard
## CRITICAL: Load Reference Files When Needed
**The quick reference below is NOT sufficient for implementation.** You MUST load relevant reference files before writing code for that component.
**Load these references based on what you're implementing:**
- **Creating widgets?** → MUST load `references/data-loading.md` first
- **Building forms/modals?** → MUST load `references/forms.md` first
- **Displaying data in tables/lists?** → MUST load `references/display-patterns.md` first
- **Selecting from large datasets?** → MUST load `references/table-selection.md` first
- **Adding navigation?** → MUST load `references/navigation.md` first
- **Styling components?** → MUST load `references/typography.md` first
**Minimum requirement:** Load at least 1-2 reference files relevant to your specific task before implementing.
## When to Use This Skill vs MedusaDocs MCP Server
**⚠️ CRITICAL: This skill should be consulted FIRST for planning and implementation.**
**Use this skill for (PRIMARY SOURCE):**
- **Planning** - Understanding how to structure admin UI features
- **Component patterns** - Widgets, pages, forms, tables, modals
- **Design system** - Typography, colors, spacing, semantic classes
- **Data loading** - Critical separate query pattern, cache invalidation
- **Best practices** - Correct vs incorrect patterns (e.g., display queries on mount)
- **Critical rules** - What NOT to do (common mistakes like conditional display queries)
**Use MedusaDocs MCP server for (SECONDARY SOURCE):**
- Specific component prop signatures after you know which component to use
- Available widget zones list
- JS SDK method details
- Configuration options reference
**Why skills come first:**
- Skills contain critical patterns like separate display/modal queries that MCP doesn't emphasize
- Skills show correct vs incorrect patterns; MCP shows what's possible
- Planning requires understanding patterns, not just API reference
## Critical Setup Rules
### SDK Client Configuration
**CRITICAL:** Always use exact configuration - different values cause errors:
```tsx
// src/admin/lib/client.ts
import Medusa from "@medusajs/js-sdk"
export const sdk = new Medusa({
baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
debug: import.meta.env.DEV,
auth: {
type: "session",
},
})
```
### pnpm Users ONLY
**CRITICAL:** Install peer dependencies BEFORE writing any code:
```bash
# Find exact version from dashboard
pnpm list @tanstack/react-query --depth=10 | grep @medusajs/dashboard
# Install that exact version
pnpm add @tanstack/react-query@[exact-version]
# If using navigation (Link component)
pnpm list react-router-dom --depth=10 | grep @medusajs/dashboard
pnpm add react-router-dom@[exact-version]
```
**npm/yarn users:** DO NOT install these packages - already available.
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Data Loading | CRITICAL | `data-` |
| 2 | Design System | CRITICAL | `design-` |
| 3 | Data Display | HIGH (includes CRITICAL price rule) | `display-` |
| 4 | Typography | HIGH | `typo-` |
| 5 | Forms & Modals | MEDIUM | `form-` |
| 6 | Selection Patterns | MEDIUM | `select-` |
## Quick Reference
### 1. Data Loading (CRITICAL)
- `data-sdk-always` - **ALWAYS use Medusa JS SDK for ALL API requests** - NEVER use regular fetch() (missing auth headers causes errors)
- `data-sdk-method-choice` - Use existing SDK methods for built-in endpoints (`sdk.admin.product.list()`), use `sdk.client.fetch()` for custom routes
- `data-display-on-mount` - Display queries MUST load on mount (no enabled condition based on UI state)
- `data-separate-queries` - Separate display queries from modal/form queries
- `data-invalidate-display` - Invalidate display queries after mutations, not just modal queries
- `data-loading-states` - Always show loading states (Spinner), not empty states
- `data-pnpm-install-first` - pnpm users MUST install @tanstack/react-query BEFORE coding
### 2. Design System (CRITICAL)
- `design-semantic-colors` - Always use semantic color classes (bg-ui-bg-base, text-ui-fg-subtle), never hardcoded
- `design-spacing` - Use px-6 py-4 for section padding, gap-2 for lists, gap-3 for items
- `design-button-size` - Always use size="small" for buttons in widgets and tables
- `design-medusa-components` - Always use Medusa UI components (Container, Button, Text), not raw HTML
### 3. Data Display (HIGH)
- `display-price-format` - **CRITICAL**: Prices from Medusa are stored as-is ($49.99 = 49.99, NOT in cents). Display them directly - NEVER divide by 100
### 4. Typography (HIGH)
- `typo-text-component` - Always use Text component from @medusajs/ui, never plain span/p tags
- `typo-labels` - Use `<Text size="small" leading="compact" weight="plus">` for labels/headings
- `typo-descriptions` - Use `<Text size="small" leading="compact" className="text-ui-fg-subtle">` for descriptions
- `typo-no-heading-widgets` - Never use Heading for small sections in widgets (use Text instead)
### 5. Forms & Modals (MEDIUM)
- `form-focusmodal-create` - Use FocusModal for creating new entities
- `form-drawer-edit` - Use Drawer for editing existing entities
- `form-disable-pending` - Always disable actions during mutations (disabled={mutation.isPending})
- `form-show-loading` - Show loading state on submit button (isLoading={mutation.isPending})
### 6. Selection Patterns (MEDIUM)
- `select-small-datasets` - Use Select component for 2-10 options (statuses, types, etc.)
- `select-large-datasets` - Use DataTable with FocusModal for large datasets (products, categories, etc.)
- `select-search-config` - Must pass search configuration to useDataTable to avoid "search not enabled" error
## Critical Data Loading Pattern
**ALWAYS follow this pattern - never load display data conditionally:**
```tsx
// ✅ CORRECT - Separate queries with proper responsibilities
const RelatedProductsWidget = ({ data: product }) => {
const [modalOpen, setModalOpen] = useState(false)
// Display query - loads on mount
const { data: displayProducts } = useQuery({
queryFn: () => fetchSelectedProducts(selectedIds),
queryKey: ["related-products-display", product.id],
// No 'enabled' condition - loads immediately
})
// Modal query - loads when needed
const { data: modalProducts } = useQuery({
queryFn: () => sdk.admin.product.list({ limit: 10, offset: 0 }),
queryKey: ["products-selection"],
enabled: modalOpen, // OK for modal-only data
})
// Mutation with proper invalidation
const updateProduct = useMutation({
mutationFn: updateFunction,
onSuccess: () => {
// Invalidate display data query to refresh UI
queryClient.invalidateQueries({ queryKey: ["related-products-display", product.id] })
// Also invalidate the entity query
queryClient.invalidateQueries({ queryKey: ["product", product.id] })
// Note: No need to invalidate modal selection query
},
})
return (
<Container>
{/* Display uses displayProducts */}
{displayProducts?.map(p => <div key={p.id}>{p.title}</div>)}
<FocusModal open={modalOpen} onOpenChange={setModalOpen}>
{/* Modal uses modalProducts */}
</FocusModal>
</Container>
)
}
// ❌ WRONG - Single query with conditional loading
const BrokenWidget = ({ data: product }) => {
const [modalOpen, setModalOpen] = useState(false)
const { data } = useQuery({
queryFn: () => sdk.admin.product.list(),
enabled: modalOpen, // ❌ Display breaks on page refresh!
})
// Trying to display from modal query
const displayItems = data?.filter(item => ids.includes(item.id)) // No data until modal opens
return <div>{displayItems?.map(...)}</div> // Empty on mount!
}
```
**Why this matters:**
- On page refresh, modal is closed, so conditional query doesn't run
- User sees empty state instead of their data
- Display depends on modal interaction (broken UX)
## Common Mistakes Checklist
Before implementing, verify you're NOT doing these:
**Data Loading:**
- [ ] Using regular fetch() instead of Medusa JS SDK (causes missing auth header errors)
- [ ] Not using existing SDK methods for built-in endpoints (e.g., using sdk.client.fetch("/admin/products") instead of sdk.admin.product.list())
- [ ] Loading display data conditionally based on modal/UI state
- [ ] Using a single query for both display and modal
- [ ] Forgetting to invalidate display queries after mutations
- [ ] Not handling loading states (showing empty instead of spinner)
- [ ] pnpm users: Not installing @tanstack/react-query before coding
**Design System:**
- [ ] Using hardcoded colors instead of semantic classes
- [ ] Forgetting size="small" on buttons in widgets
- [ ] Not using px-6 py-4 for section padding
- [ ] Using raw HTML elements instead of Medusa UI components
**Data Display:**
- [ ] **CRITICAL**: Dividing prices by 100 when displaying (prices are stored as-is: $49.99 = 49.99, NOT in cents)
**Typography:**
- [ ] Using plain span/p tags instead of Text component
- [ ] Not using weight="plus" for labels
- [ ] Not using text-ui-fg-subtle for descriptions
- [ ] Using Heading in small widget sections
**Forms:**
- [ ] Using Drawer for creating (should use FocusModal)
- [ ] Using FocusModal for editing (should use Drawer)
- [ ] Not disabling buttons during mutations
- [ ] Not showing loading state on submit
**Selection:**
- [ ] Using DataTable for <10 items (overkill)
- [ ] Using Select for >10 items (poor UX)
- [ ] Not configuring search in useDataTable (causes error)
## Reference Files Available
Load these for detailed patterns:
```
references/data-loading.md - useQuery/useMutation patterns, cache invalidation
references/forms.md - FocusModal/Drawer patterns, validation
references/table-selection.md - Complete DataTable selection pattern
references/display-patterns.md - Lists, tables, cards for entities
references/typography.md - Text component patterns
references/navigation.md - Link, useNavigate, useParams patterns
```
Each reference contains:
- Step-by-step implementation guides
- Correct vs incorrect code examples
- Common mistakes and solutions
- Complete working examples
## Integration with Backend
**⚠️ CRITICAL: ALWAYS use the Medusa JS SDK for ALL API requests - NEVER use regular fetch()**
Admin UI connects to backend API routes using the SDK:
```tsx
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
// ✅ CORRECT - Built-in endpoint: Use existing SDK method
const { data: product } = useQuery({
queryKey: ["product", productId],
queryFn: () => sdk.admin.product.retrieve(productId),
})
// ✅ CORRECT - Custom endpoint: Use sdk.client.fetch()
const { data: reviews } = useQuery({
queryKey: ["reviews", product.id],
queryFn: () => sdk.client.fetch(`/admin/products/${product.id}/reviews`),
})
// ❌ WRONG - Using regular fetch
const { data } = useQuery({
queryKey: ["reviews", product.id],
queryFn: () => fetch(`http://localhost:9000/admin/products/${product.id}/reviews`),
// ❌ Error: Missing Authorization header!
})
// Mutation to custom backend route
const createReview = useMutation({
mutationFn: (data) => sdk.client.fetch("/admin/reviews", {
method: "POST",
body: data
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["reviews", product.id] })
toast.success("Review created")
},
})
```
**Why the SDK is required:**
- Admin routes need `Authorization` and session cookie headers
- Store routes need `x-publishable-api-key` header
- SDK handles all required headers automatically
- Regular fetch() without headers → authentication/authorization errors
- Using existing SDK methods provides better type safety
**When to use what:**
- **Built-in endpoints**: Use existing SDK methods (`sdk.admin.product.list()`, `sdk.store.product.list()`)
- **Custom endpoints**: Use `sdk.client.fetch()` for your custom API routes
**For implementing backend API routes**, load the `building-with-medusa` skill.
## Widget vs UI Route
**Widgets** extend existing admin pages:
```tsx
// src/admin/widgets/custom-widget.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { DetailWidgetProps } from "@medusajs/framework/types"
const MyWidget = ({ data }: DetailWidgetProps<HttpTypes.AdminProduct>) => {
return <Container>Widget content</Container>
}
export const config = defineWidgetConfig({
zone: "product.details.after",
})
export default MyWidget
```
**UI Routes** create new admin pages:
```tsx
// src/admin/routes/custom-page/page.tsx
import { defineRouteConfig } from "@medusajs/admin-sdk"
const CustomPage = () => {
return <div>Page content</div>
}
export const config = defineRouteConfig({
label: "Custom Page",
})
export default CustomPage
```
## Common Issues & Solutions
**"Cannot find module" errors (pnpm users):**
- Install peer dependencies BEFORE coding
- Use exact versions from dashboard
**"No QueryClient set" error:**
- pnpm: Install @tanstack/react-query
- npm/yarn: Remove incorrectly installed package
**"DataTable.Search not enabled":**
- Must pass search configuration to useDataTable
**Widget not refreshing:**
- Invalidate display queries, not just modal queries
- Include all dependencies in query keys
**Display empty on refresh:**
- Display query has conditional `enabled` based on UI state
- Remove condition - display data must load on mount
## Next Steps - Testing Your Implementation
**After successfully implementing a feature, always provide these next steps to the user:**
### 1. Start the Development Server
If the server isn't already running, start it:
```bash
npm run dev # or pnpm dev / yarn dev
```
### 2. Access the Admin Dashboard
Open your browser and navigate to:
- **Admin Dashboard:** http://localhost:9000/app
Log in with your admin credentials.
### 3. Navigate to Your Custom UI
**For Widgets:**
Navigate to the page where your widget is displayed. Common widget zones:
- **Product widgets:** Go to Products → Select a product → Your widget appears in the zone you configured (e.g., `product.details.after`)
- **Order widgets:** Go to Orders → Select an order → Your widget appears in the configured zone
- **Customer widgets:** Go to Customers → Select a customer → Your widget appears in the configured zone
**For UI Routes (Custom Pages):**
- Look for your custom page in the admin sidebar/navigation (based on the `label` you configured)
- Or navigate directly to: `http://localhost:9000/app/[your-route-path]`
### 4. Test Functionality
Depending on what was implemented, test:
- **Forms:** Try creating/editing entities, verify validation and error messages
- **Tables:** Test pagination, search, sorting, and row selection
- **Data display:** Verify data loads correctly and refreshes after mutations
- **Modals:** Open FocusModal/Drawer, test form submission, verify data updates
- **Navigation:** Click links and verify routing works correctly
### Format for Presenting Next Steps
Always present next steps in a clear, actionable format after implementation:
```markdown
## Implementation Complete
The [feature name] has been successfully implemented. Here's how to see it:
### Start the Development Server
[command based on package manager]
### Access the Admin Dashboard
Open http://localhost:9000/app in your browser and log in.
### View Your Custom UI
**For Widgets:**
1. Navigate to [specific admin page, e.g., "Products"]
2. Select [an entity, e.g., "any product"]
3. Scroll to [zone location, e.g., "the bottom of the page"]
4. You'll see your "[widget name]" widget
**For UI Routes:**
1. Look for "[page label]" in the admin navigation
2. Or navigate directly to http://localhost:9000/app/[route-path]
### What to Test
1. [Specific test case 1]
2. [Specific test case 2]
3. [Specific test case 3]
```

View File

@@ -0,0 +1,530 @@
# Data Loading Principles and Patterns
## Contents
- [Fundamental Rules](#fundamental-rules)
- [Think Before You Code Checklist](#think-before-you-code-checklist)
- [Common Mistake vs Correct Pattern](#common-mistake-vs-correct-pattern)
- [Working with Tanstack Query](#working-with-tanstack-query)
- [Fetching Data with useQuery](#fetching-data-with-usequery)
- [Basic Query](#basic-query)
- [Paginated Query](#paginated-query)
- [Query with Dependencies](#query-with-dependencies)
- [Fetching Multiple Items by IDs](#fetching-multiple-items-by-ids)
- [Updating Data with useMutation](#updating-data-with-usemutation)
- [Basic Mutation](#basic-mutation)
- [Mutation with Loading State](#mutation-with-loading-state)
- [Create Mutation](#create-mutation)
- [Delete Mutation](#delete-mutation)
- [Cache Invalidation Guidelines](#cache-invalidation-guidelines)
- [Important Notes about Metadata](#important-notes-about-metadata)
- [Common Patterns](#common-patterns)
- [Pattern: Fetching Data with Pagination](#pattern-fetching-data-with-pagination)
- [Pattern: Search with Debounce](#pattern-search-with-debounce)
- [Pattern: Updating Metadata with useMutation](#pattern-updating-metadata-with-usemutation)
- [Common Issues & Solutions](#common-issues--solutions)
- [Complete Example: Widget with Separate Queries](#complete-example-widget-with-separate-queries)
## Fundamental Rules
1. **ALWAYS use the Medusa JS SDK** - NEVER use regular fetch() for API requests (missing headers causes authentication/authorization errors)
2. **Display data must load on mount** - Any data shown in the widget's main UI must be fetched when the component mounts, not conditionally
3. **Separate concerns** - Modal/form data queries should be independent from display data queries
4. **Handle reference data properly** - When storing IDs/references (in metadata or elsewhere), you must fetch the full entities to display them
5. **Always show loading states** - Users should see loading indicators, not empty states, while data is being fetched
6. **Invalidate the right queries** - After mutations, invalidate the queries that provide display data, not just the modal queries
## Think Before You Code Checklist
Before implementing any widget that displays data:
- [ ] Am I using the Medusa JS SDK for all API requests (not regular fetch)?
- [ ] For built-in endpoints, am I using existing SDK methods (not sdk.client.fetch)?
- [ ] What data needs to be visible immediately?
- [ ] Where is this data stored? (metadata, separate endpoint, related entities)
- [ ] If storing IDs, how will I fetch the full entities for display?
- [ ] Are my display queries separate from interaction queries?
- [ ] Have I added loading states for all data fetches?
- [ ] Which queries need invalidation after updates to refresh the display?
## Common Mistake vs Correct Pattern
### ❌ WRONG - Single query for both display and modal:
```tsx
// This breaks on page refresh!
const { data } = useQuery({
queryFn: () => sdk.admin.product.list(),
enabled: modalOpen, // Display won't work on mount!
})
// Trying to display filtered data from modal query
const displayItems = data?.filter((item) => ids.includes(item.id)) // No data until modal opens
```
**Why this is wrong:**
- On page refresh, modal is closed, so query doesn't run
- User sees empty state instead of their data
- Display depends on modal interaction
### ✅ CORRECT - Separate queries with proper invalidation:
```tsx
// Display data - loads immediately
const { data: displayData } = useQuery({
queryFn: () => fetchDisplayData(),
queryKey: ["display-data", product.id],
// No 'enabled' condition - loads on mount
})
// Modal data - loads when needed
const { data: modalData } = useQuery({
queryFn: () => fetchModalData(),
queryKey: ["modal-data"],
enabled: modalOpen, // OK for modal-only data
})
// Mutation with proper cache invalidation
const updateMutation = useMutation({
mutationFn: updateFunction,
onSuccess: () => {
// Invalidate display data query to refresh UI
queryClient.invalidateQueries({ queryKey: ["display-data", product.id] })
// Also invalidate the entity if it caches the data
queryClient.invalidateQueries({ queryKey: ["product", product.id] })
},
})
```
**Why this is correct:**
- Display query runs immediately on component mount
- Modal query only runs when needed
- Proper invalidation ensures UI updates after changes
- Each query has a clear, separate responsibility
## Using the Medusa JS SDK
**⚠️ CRITICAL: ALWAYS use the Medusa JS SDK for ALL API requests - NEVER use regular fetch()**
### Why the SDK is Required
- **Admin routes** require `Authorization` header and session cookie - SDK adds them automatically
- **Store routes** require `x-publishable-api-key` header - SDK adds them automatically
- **Regular fetch()** doesn't include these headers → authentication/authorization errors
- Using existing SDK methods provides better type safety and autocomplete
### When to Use What
```tsx
import { sdk } from "../lib/client"
// ✅ CORRECT - Built-in endpoint: Use existing SDK method
const product = await sdk.admin.product.retrieve(productId, {
fields: "+metadata,+variants.*"
})
// ✅ CORRECT - Custom endpoint: Use sdk.client.fetch()
const reviews = await sdk.client.fetch(`/admin/products/${productId}/reviews`)
// ❌ WRONG - Using regular fetch for ANY endpoint
const response = await fetch(`http://localhost:9000/admin/products/${productId}`)
// ❌ Error: Missing Authorization header!
```
### SDK Method Selection
**For built-in Medusa endpoints:**
- Use existing SDK methods: `sdk.admin.product.list()`, `sdk.store.product.list()`, etc.
- Provides type safety, autocomplete, and proper header handling
- Reference: [Medusa JS SDK Documentation](https://docs.medusajs.com/resources/medusa-js-sdk)
**For custom API routes:**
- Use `sdk.client.fetch()` for your custom endpoints
- SDK still handles all required headers (auth, API keys)
- Pass plain objects to body (SDK handles JSON serialization)
## Working with Tanstack Query
Admin widgets and routes have Tanstack Query pre-configured.
**⚠️ pnpm Users**: You MUST install `@tanstack/react-query` BEFORE using `useQuery` or `useMutation`. Install with exact version from dashboard:
```bash
pnpm list @tanstack/react-query --depth=10 | grep @medusajs/dashboard
pnpm add @tanstack/react-query@[exact-version]
```
**npm/yarn Users**: DO NOT install `@tanstack/react-query` - it's already available through dashboard dependencies.
## Fetching Data with useQuery
### Basic Query
```tsx
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/client"
const { data, isLoading, error } = useQuery({
queryFn: () => sdk.admin.product.retrieve(productId, {
fields: "+metadata,+variants.*",
}),
queryKey: ["product", productId],
})
```
### Paginated Query
```tsx
const limit = 15
const offset = pagination.pageIndex * limit
const { data: products } = useQuery({
queryFn: () =>
sdk.admin.product.list({
limit,
offset,
q: searchTerm, // for search
}),
queryKey: ["products", limit, offset, searchTerm],
keepPreviousData: true, // Prevents UI flicker during pagination
})
```
### Query with Dependencies
```tsx
// Only fetch if productId exists
const { data } = useQuery({
queryFn: () => sdk.admin.product.retrieve(productId),
queryKey: ["product", productId],
enabled: !!productId, // Only run when productId is truthy
})
```
### Fetching Multiple Items by IDs
```tsx
// For display - fetch specific items by IDs
const { data: displayProducts } = useQuery({
queryFn: async () => {
if (selectedIds.length === 0) return { products: [] }
const response = await sdk.admin.product.list({
id: selectedIds, // Fetch only the selected products
limit: selectedIds.length,
})
return response
},
queryKey: ["related-products-display", selectedIds],
enabled: selectedIds.length > 0, // Only fetch if there are IDs
})
```
## Updating Data with useMutation
### Basic Mutation
```tsx
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "@medusajs/ui"
const queryClient = useQueryClient()
const updateProduct = useMutation({
mutationFn: (payload) => sdk.admin.product.update(productId, payload),
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ["product", productId] })
toast.success("Product updated successfully")
},
onError: (error) => {
toast.error(error.message || "Failed to update product")
},
})
// Usage
const handleSave = () => {
updateProduct.mutate({
metadata: {
...existingMetadata,
new_field: "value",
},
})
}
```
### Mutation with Loading State
```tsx
<Button
onClick={handleSave}
isLoading={updateProduct.isPending}
>
Save
</Button>
```
### Create Mutation
```tsx
const createProduct = useMutation({
mutationFn: (data) => sdk.admin.product.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
toast.success("Product created successfully")
setOpen(false)
},
})
```
### Delete Mutation
```tsx
const deleteProduct = useMutation({
mutationFn: (id) => sdk.admin.product.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
toast.success("Product deleted")
},
})
```
## Cache Invalidation Guidelines
After mutations, invalidate the queries that affect what the user sees:
```tsx
onSuccess: () => {
// Invalidate the entity itself if it stores the data
queryClient.invalidateQueries({ queryKey: ["product", productId] })
// Invalidate display-specific queries
queryClient.invalidateQueries({ queryKey: ["related-products", productId] })
// Don't need to invalidate modal selection queries
// queryClient.invalidateQueries({ queryKey: ["products-list"] }) // Not needed
}
```
**Key Points:**
- Use specific query keys with IDs for targeted invalidation
- Invalidate both the entity and display data queries when needed
- Consider what the user sees and ensure those queries refresh
- Modal/selection queries typically don't need invalidation
## Important Notes about Metadata
- When updating nested objects in metadata, pass the entire object (Medusa doesn't merge nested objects)
- To remove a metadata property, set it to an empty string
- Metadata is stored as JSONB in the database
**Example: Updating Metadata**
```tsx
// ✅ CORRECT - Spread existing metadata
updateProduct.mutate({
metadata: {
...product.metadata,
new_field: "value",
},
})
// ❌ WRONG - Overwrites all metadata
updateProduct.mutate({
metadata: {
new_field: "value", // All other fields lost!
},
})
```
## Common Patterns
### Pattern: Fetching Data with Pagination
```tsx
const limit = 15
const offset = pagination.pageIndex * limit
const { data } = useQuery({
queryFn: () => sdk.admin.product.list({ limit, offset }),
queryKey: ["products", limit, offset],
keepPreviousData: true, // Prevents UI flicker during pagination
})
```
### Pattern: Search with Debounce
```tsx
import { useDebouncedValue } from "@mantine/hooks" // or implement your own
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 300)
const { data } = useQuery({
queryFn: () => sdk.admin.product.list({ q: debouncedSearch }),
queryKey: ["products", debouncedSearch],
})
```
### Pattern: Updating Metadata with useMutation
```tsx
const updateMetadata = useMutation({
mutationFn: (metadata) => sdk.admin.product.update(productId, { metadata }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["product", productId] })
toast.success("Updated successfully")
},
})
```
## Common Issues & Solutions
### Authentication/Authorization errors when fetching data
**Symptoms:**
- API returns 401 Unauthorized or 403 Forbidden
- "Missing x-publishable-api-key header" error
- "Unauthorized" error on admin routes
**Cause:** Using regular `fetch()` instead of the Medusa JS SDK
**Solution:**
```tsx
// ❌ WRONG - Missing required headers
const { data } = useQuery({
queryFn: () => fetch('http://localhost:9000/admin/products').then(r => r.json()),
queryKey: ["products"]
})
// ✅ CORRECT - SDK handles headers automatically
const { data } = useQuery({
queryFn: () => sdk.admin.product.list(),
queryKey: ["products"]
})
// ✅ CORRECT - For custom routes
const { data } = useQuery({
queryFn: () => sdk.client.fetch('/admin/custom-route'),
queryKey: ["custom-data"]
})
```
### "No QueryClient set, use QueryClientProvider to set one"
- **pnpm users**: You forgot to install `@tanstack/react-query` before implementing. Install it now with the exact version from dashboard
- **npm/yarn users**: You incorrectly installed `@tanstack/react-query` - remove it from package.json
- Never wrap your component in QueryClientProvider - it's already provided
### Search not filtering results
- The search happens server-side via the `q` parameter
- Make sure to pass the search value in your queryFn:
```tsx
queryFn: () => sdk.admin.product.list({ q: searchValue })
```
### Metadata updates not working
- Always pass the complete metadata object (partial updates aren't merged)
- To remove a field, set it to an empty string, not null or undefined
### Widget not refreshing after mutation
- Use queryClient.invalidateQueries() with the correct query key
- Ensure your query key includes all dependencies (search, pagination, etc.)
### Data shows empty on page refresh
- Your query has `enabled: modalOpen` or similar condition
- Display data should NEVER be conditionally enabled based on UI state
- Move conditional queries to modals/forms only
## Complete Example: Widget with Separate Queries
```tsx
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { useState, useMemo } from "react"
import { Container, Heading, Button, FocusModal, toast } from "@medusajs/ui"
import { sdk } from "../lib/client"
const RelatedProductsWidget = ({ data: product }) => {
const [open, setOpen] = useState(false)
const queryClient = useQueryClient()
// Parse existing related product IDs from metadata
const relatedIds = useMemo(() => {
if (product?.metadata?.related_product_ids) {
try {
const ids = JSON.parse(product.metadata.related_product_ids)
return Array.isArray(ids) ? ids : []
} catch {
return []
}
}
return []
}, [product?.metadata?.related_product_ids])
// Query 1: Fetch selected products for display (loads on mount)
const { data: displayProducts } = useQuery({
queryFn: async () => {
if (relatedIds.length === 0) return { products: [] }
const response = await sdk.admin.product.list({
id: relatedIds,
limit: relatedIds.length,
})
return response
},
queryKey: ["related-products-display", relatedIds],
enabled: relatedIds.length > 0,
})
// Query 2: Fetch products for modal selection (only when modal is open)
const { data: modalProducts, isLoading } = useQuery({
queryFn: () => sdk.admin.product.list({ limit: 10, offset: 0 }),
queryKey: ["products-selection"],
enabled: open, // Only load when modal is open
})
// Mutation to update the product metadata
const updateProduct = useMutation({
mutationFn: (relatedProductIds) => {
return sdk.admin.product.update(product.id, {
metadata: {
...product.metadata,
related_product_ids: JSON.stringify(relatedProductIds),
},
})
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["product", product.id] })
queryClient.invalidateQueries({ queryKey: ["related-products-display"] })
toast.success("Related products updated")
setOpen(false)
},
})
return (
<Container>
<div className="flex items-center justify-between">
<Heading>Related Products</Heading>
<Button onClick={() => setOpen(true)}>Edit</Button>
</div>
{/* Display current selection */}
<div>
{displayProducts?.products.map((p) => (
<div key={p.id}>{p.title}</div>
))}
</div>
{/* Modal for selection */}
<FocusModal open={open} onOpenChange={setOpen}>
{/* Modal content with selection UI */}
</FocusModal>
</Container>
)
}
```

View File

@@ -0,0 +1,436 @@
# Displaying Entities - Patterns and Components
## Contents
- [When to Use Each Pattern](#when-to-use-each-pattern)
- [DataTable Pattern](#datatable-pattern)
- [Complete DataTable Implementation](#complete-datatable-implementation)
- [DataTable Troubleshooting](#datatable-troubleshooting)
- [Simple List Patterns](#simple-list-patterns)
- [Product/Variant List Item](#productvariant-list-item)
- [Simple Text List (No Thumbnails)](#simple-text-list-no-thumbnails)
- [Compact List (No Cards)](#compact-list-no-cards)
- [Grid Display](#grid-display)
- [Key Design Elements](#key-design-elements)
- [Empty States](#empty-states)
- [Loading States](#loading-states)
- [Conditional Rendering Based on Count](#conditional-rendering-based-on-count)
- [Common Class Patterns](#common-class-patterns)
## When to Use Each Pattern
**Use DataTable when:**
- Displaying potentially many entries (>5-10 items)
- Users need to search, filter, or paginate
- Bulk actions are needed (select multiple, delete, etc.)
- Displaying in a main list view
**Use simple list components when:**
- Displaying a few entries (<5-10 items)
- In a widget or sidebar context
- As a preview or summary
- Space is limited
## DataTable Pattern
**⚠️ pnpm Users**: DataTable examples may use `react-router-dom` for navigation. Install it BEFORE implementing if needed.
### Complete DataTable Implementation
```tsx
import {
DataTable,
DataTableRowSelectionState,
DataTablePaginationState,
createDataTableColumnHelper,
useDataTable,
} from "@medusajs/ui"
import { useState, useMemo } from "react"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/client"
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>()
const columns = [
columnHelper.select(), // For row selection
columnHelper.accessor("title", {
header: "Title",
}),
columnHelper.accessor("status", {
header: "Status",
}),
columnHelper.accessor("created_at", {
header: "Created",
cell: ({ getValue }) => new Date(getValue()).toLocaleDateString(),
}),
]
export function ProductTable() {
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(
{}
)
const [searchValue, setSearchValue] = useState("")
const [pagination, setPagination] = useState<DataTablePaginationState>({
pageIndex: 0,
pageSize: 15,
})
const limit = pagination.pageSize
const offset = pagination.pageIndex * limit
// Fetch products with search and pagination
const { data, isLoading } = useQuery({
queryFn: () =>
sdk.admin.product.list({
limit,
offset,
q: searchValue || undefined, // Search query
}),
queryKey: ["products", limit, offset, searchValue],
keepPreviousData: true, // Smooth pagination
})
const table = useDataTable({
data: data?.products || [],
columns,
getRowId: (product) => product.id,
rowCount: data?.count || 0,
isLoading,
rowSelection: {
state: rowSelection,
onRowSelectionChange: setRowSelection,
},
search: {
state: searchValue,
onSearchChange: setSearchValue,
},
pagination: {
state: pagination,
onPaginationChange: setPagination,
},
})
return (
<DataTable instance={table}>
<DataTable.Toolbar>
<div className="flex gap-2">
<DataTable.Search placeholder="Search products..." />
</div>
</DataTable.Toolbar>
<DataTable.Table />
<DataTable.Pagination />
</DataTable>
)
}
```
### DataTable Troubleshooting
**"DataTable.Search was rendered but search is not enabled"**
You must pass search state configuration to useDataTable:
```tsx
search: {
state: searchValue,
onSearchChange: setSearchValue,
}
```
**"Cannot destructure property 'pageIndex' of pagination as it is undefined"**
Always initialize pagination state with both properties:
```tsx
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 15,
})
```
## Simple List Patterns
### Product/Variant List Item
For displaying a small list of products or variants with thumbnails:
```tsx
import { Thumbnail, Text } from "@medusajs/ui"
import { TriangleRightMini } from "@medusajs/icons"
import { Link } from "react-router-dom"
// Component for displaying a product variant
const ProductVariantItem = ({ variant, link }) => {
const Inner = (
<div className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-2 transition-colors">
<div className="flex items-center gap-3">
<div className="shadow-elevation-card-rest rounded-md">
<Thumbnail src={variant.product?.thumbnail} />
</div>
<div className="flex flex-1 flex-col">
<Text size="small" leading="compact" weight="plus">
{variant.title}
</Text>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{variant.options.map((o) => o.value).join(" ⋅ ")}
</Text>
</div>
<div className="size-7 flex items-center justify-center">
<TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
</div>
</div>
</div>
)
if (!link) {
return <div key={variant.id}>{Inner}</div>
}
return (
<Link
to={link}
key={variant.id}
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
>
{Inner}
</Link>
)
}
// Usage in a widget
const RelatedProductsDisplay = ({ products }) => {
if (products.length > 10) {
// Use DataTable for many items
return <ProductDataTable products={products} />
}
// Use simple list for few items
return (
<div className="flex flex-col gap-2">
{products.map((product) => (
<ProductVariantItem
key={product.id}
variant={product}
link={`/products/${product.id}`}
/>
))}
</div>
)
}
```
### Simple Text List (No Thumbnails)
For entities without images (categories, regions, etc.):
```tsx
import { Text } from "@medusajs/ui"
import { TriangleRightMini } from "@medusajs/icons"
import { Link } from "react-router-dom"
const SimpleListItem = ({ title, description, link }) => {
const Inner = (
<div className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-3 transition-colors">
<div className="flex items-center gap-3">
<div className="flex flex-1 flex-col gap-y-1">
<Text size="small" leading="compact" weight="plus">
{title}
</Text>
{description && (
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{description}
</Text>
)}
</div>
<div className="size-7 flex items-center justify-center">
<TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
</div>
</div>
</div>
)
if (!link) {
return <div>{Inner}</div>
}
return (
<Link
to={link}
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
>
{Inner}
</Link>
)
}
// Usage
<div className="flex flex-col gap-2">
{categories.map((cat) => (
<SimpleListItem
key={cat.id}
title={cat.name}
description={cat.description}
link={`/categories/${cat.id}`}
/>
))}
</div>
```
### Compact List (No Cards)
For very compact displays:
```tsx
import { Text } from "@medusajs/ui"
<div className="flex flex-col gap-y-2">
{items.map((item) => (
<div key={item.id} className="flex items-center justify-between">
<Text size="small" leading="compact" weight="plus">
{item.title}
</Text>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{item.metadata}
</Text>
</div>
))}
</div>
```
### Grid Display
For displaying items in a grid:
```tsx
<div className="grid grid-cols-2 gap-4">
{items.map((item) => (
<div
key={item.id}
className="shadow-elevation-card-rest bg-ui-bg-component rounded-md p-4"
>
<div className="flex flex-col gap-y-2">
<Thumbnail src={item.thumbnail} />
<Text size="small" leading="compact" weight="plus">
{item.title}
</Text>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{item.description}
</Text>
</div>
</div>
))}
</div>
```
## Key Design Elements
### For Product/Variant displays:
- Always show the thumbnail using `<Thumbnail />` component
- Display title with `<Text size="small" leading="compact" weight="plus">`
- Show secondary info with `<Text size="small" leading="compact" className="text-ui-fg-subtle">`
- Use `shadow-elevation-card-rest` for card elevation
- Include hover states with `bg-ui-bg-component-hover`
- Add navigation indicators (arrows) when items are clickable
### For other entities:
- Use similar card patterns but adapt the content
- Keep consistent spacing (`gap-3` for items, `gap-2` for lists)
- Always use the Text component with correct typography patterns
- Maintain visual hierarchy with `weight="plus"` for primary and `text-ui-fg-subtle` for secondary text
## Empty States
Always handle empty states gracefully:
```tsx
{items.length === 0 ? (
<Text size="small" leading="compact" className="text-ui-fg-subtle">
No items to display
</Text>
) : (
<div className="flex flex-col gap-2">
{items.map((item) => (
<ItemDisplay key={item.id} item={item} />
))}
</div>
)}
```
## Loading States
Show loading states while data is being fetched:
```tsx
import { Spinner } from "@medusajs/ui"
{isLoading ? (
<div className="flex items-center justify-center p-8">
<Spinner />
</div>
) : (
<div className="flex flex-col gap-2">
{items.map((item) => (
<ItemDisplay key={item.id} item={item} />
))}
</div>
)}
```
## Conditional Rendering Based on Count
```tsx
const DisplayComponent = ({ items }) => {
// Use DataTable for many items
if (items.length > 10) {
return <ItemsDataTable items={items} />
}
// Use simple list for few items
if (items.length > 0) {
return (
<div className="flex flex-col gap-2">
{items.map((item) => (
<SimpleListItem key={item.id} item={item} />
))}
</div>
)
}
// Empty state
return (
<Text size="small" leading="compact" className="text-ui-fg-subtle">
No items to display
</Text>
)
}
```
## Common Class Patterns
### Card with elevation and hover
```tsx
className="shadow-elevation-card-rest bg-ui-bg-component rounded-md transition-colors hover:bg-ui-bg-component-hover"
```
### Flex container with consistent spacing
```tsx
className="flex flex-col gap-2" // For vertical lists
className="flex items-center gap-3" // For horizontal items
```
### Focus states for interactive elements
```tsx
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md"
```
### RTL support for directional icons
```tsx
className="text-ui-fg-muted rtl:rotate-180"
```

View File

@@ -0,0 +1,400 @@
# Forms and Modal Patterns
## Contents
- [FocusModal vs Drawer](#focusmodal-vs-drawer)
- [Edit Button Patterns](#edit-button-patterns)
- [Simple Edit Button (top right corner)](#simple-edit-button-top-right-corner)
- [Dropdown Menu with Actions](#dropdown-menu-with-actions)
- [Select Component for Small Datasets](#select-component-for-small-datasets)
- [FocusModal Example](#focusmodal-example)
- [Drawer Example](#drawer-example)
- [Form with Validation and Loading States](#form-with-validation-and-loading-states)
- [Key Form Patterns](#key-form-patterns)
## FocusModal vs Drawer
**FocusModal** - Use for creating new entities:
- Full-screen modal
- More space for complex forms
- Better for multi-step flows
**Drawer** - Use for editing existing entities:
- Side panel that slides in from right
- Quick edits without losing context
- Better for single-field updates
**Rule of thumb:** FocusModal for creating, Drawer for editing.
## Edit Button Patterns
Data displayed in a container should not be editable directly. Instead, use an "Edit" button. This can be:
### Simple Edit Button (top right corner)
```tsx
import { Button } from "@medusajs/ui"
import { PencilSquare } from "@medusajs/icons"
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Section Title</Heading>
<Button
size="small"
variant="secondary"
onClick={() => setOpen(true)}
>
<PencilSquare />
</Button>
</div>
```
### Dropdown Menu with Actions
```tsx
import { EllipsisHorizontal, PencilSquare, Plus, Trash } from "@medusajs/icons"
import { DropdownMenu, IconButton } from "@medusajs/ui"
export function DropdownMenuDemo() {
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<IconButton size="small" variant="transparent">
<EllipsisHorizontal />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item className="gap-x-2">
<PencilSquare className="text-ui-fg-subtle" />
Edit
</DropdownMenu.Item>
<DropdownMenu.Item className="gap-x-2">
<Plus className="text-ui-fg-subtle" />
Add
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item className="gap-x-2">
<Trash className="text-ui-fg-subtle" />
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
)
}
```
## Select Component for Small Datasets
For selecting from 2-10 options (statuses, types, etc.), use the Select component:
```tsx
import { Select } from "@medusajs/ui"
<Select>
<Select.Trigger>
<Select.Value placeholder="Select status" />
</Select.Trigger>
<Select.Content>
{items.map((item) => (
<Select.Item key={item.value} value={item.value}>
{item.label}
</Select.Item>
))}
</Select.Content>
</Select>
```
**For larger datasets** (Products, Categories, Regions, etc.), use DataTable with FocusModal for search and pagination. See [table-selection.md](table-selection.md) for the complete pattern.
## FocusModal Example
```tsx
import { FocusModal, Button, Input, Label } from "@medusajs/ui"
import { useState } from "react"
const MyWidget = () => {
const [open, setOpen] = useState(false)
const [formData, setFormData] = useState({ title: "" })
const handleSubmit = () => {
// Handle form submission
console.log(formData)
setOpen(false)
}
return (
<>
<Button onClick={() => setOpen(true)}>
Create New
</Button>
<FocusModal open={open} onOpenChange={setOpen}>
<FocusModal.Content>
<div className="flex h-full flex-col overflow-hidden">
<FocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<Button size="small" variant="secondary">
Cancel
</Button>
</FocusModal.Close>
<Button size="small" onClick={handleSubmit}>
Save
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex-1 overflow-auto">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-2">
<Label>Title</Label>
<Input
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
/>
</div>
{/* More form fields */}
</div>
</FocusModal.Body>
</div>
</FocusModal.Content>
</FocusModal>
</>
)
}
```
## Drawer Example
```tsx
import { Drawer, Button, Input, Label } from "@medusajs/ui"
import { useState } from "react"
const MyWidget = ({ data }) => {
const [open, setOpen] = useState(false)
const [formData, setFormData] = useState({ title: data.title })
const handleSubmit = () => {
// Handle form submission
console.log(formData)
setOpen(false)
}
return (
<>
<Button onClick={() => setOpen(true)}>
Edit
</Button>
<Drawer open={open} onOpenChange={setOpen}>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Edit Settings</Drawer.Title>
</Drawer.Header>
<Drawer.Body className="flex-1 overflow-auto p-4">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-2">
<Label>Title</Label>
<Input
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
/>
</div>
{/* More form fields */}
</div>
</Drawer.Body>
<Drawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<Button size="small" variant="secondary">
Cancel
</Button>
</Drawer.Close>
<Button size="small" onClick={handleSubmit}>
Save
</Button>
</div>
</Drawer.Footer>
</Drawer.Content>
</Drawer>
</>
)
}
```
## Form with Validation and Loading States
```tsx
import { FocusModal, Button, Input, Label, Text, toast } from "@medusajs/ui"
import { useState } from "react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/client"
const CreateProductWidget = () => {
const [open, setOpen] = useState(false)
const [formData, setFormData] = useState({
title: "",
description: "",
})
const [errors, setErrors] = useState({})
const queryClient = useQueryClient()
const createProduct = useMutation({
mutationFn: (data) => sdk.admin.product.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
toast.success("Product created successfully")
setOpen(false)
setFormData({ title: "", description: "" })
setErrors({})
},
onError: (error) => {
toast.error(error.message || "Failed to create product")
},
})
const handleSubmit = () => {
// Validate
const newErrors = {}
if (!formData.title) newErrors.title = "Title is required"
if (!formData.description) newErrors.description = "Description is required"
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
createProduct.mutate(formData)
}
return (
<>
<Button onClick={() => setOpen(true)}>
Create Product
</Button>
<FocusModal open={open} onOpenChange={setOpen}>
<FocusModal.Content>
<div className="flex h-full flex-col overflow-hidden">
<FocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<Button
size="small"
variant="secondary"
disabled={createProduct.isPending}
>
Cancel
</Button>
</FocusModal.Close>
<Button
size="small"
onClick={handleSubmit}
isLoading={createProduct.isPending}
>
Save
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex-1 overflow-auto">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-2">
<Label>Title *</Label>
<Input
value={formData.title}
onChange={(e) => {
setFormData({ ...formData, title: e.target.value })
setErrors({ ...errors, title: undefined })
}}
/>
{errors.title && (
<Text size="small" className="text-ui-fg-error">
{errors.title}
</Text>
)}
</div>
<div className="flex flex-col gap-y-2">
<Label>Description *</Label>
<Input
value={formData.description}
onChange={(e) => {
setFormData({ ...formData, description: e.target.value })
setErrors({ ...errors, description: undefined })
}}
/>
{errors.description && (
<Text size="small" className="text-ui-fg-error">
{errors.description}
</Text>
)}
</div>
</div>
</FocusModal.Body>
</div>
</FocusModal.Content>
</FocusModal>
</>
)
}
```
## Key Form Patterns
### Always Disable Actions During Mutations
```tsx
<Button
disabled={mutation.isPending}
onClick={handleAction}
>
Action
</Button>
```
### Show Loading State on Submit Button
```tsx
<Button
isLoading={mutation.isPending}
onClick={handleSubmit}
>
Save
</Button>
```
### Clear Form After Success
```tsx
onSuccess: () => {
setFormData(initialState)
setErrors({})
setOpen(false)
}
```
### Validate Before Submitting
```tsx
const handleSubmit = () => {
const errors = validateForm(formData)
if (Object.keys(errors).length > 0) {
setErrors(errors)
return
}
mutation.mutate(formData)
}
```
### Clear Field Errors on Input Change
```tsx
<Input
value={formData.field}
onChange={(e) => {
setFormData({ ...formData, field: e.target.value })
setErrors({ ...errors, field: undefined }) // Clear error
}}
/>
```

View File

@@ -0,0 +1,496 @@
# Navigation and Routing
## Contents
- [Pre-Implementation Requirements for pnpm](#pre-implementation-requirements-for-pnpm)
- [Basic Navigation with Link Component](#basic-navigation-with-link-component)
- [Programmatic Navigation](#programmatic-navigation)
- [Accessing Route Parameters](#accessing-route-parameters)
- [Linking to Built-in Admin Pages](#linking-to-built-in-admin-pages)
- [Navigation from Widgets](#navigation-from-widgets)
- [Common Navigation Patterns](#common-navigation-patterns)
## Pre-Implementation Requirements for pnpm
**⚠️ pnpm Users**: Navigation requires `react-router-dom`. Install BEFORE implementing:
```bash
pnpm list react-router-dom --depth=10 | grep @medusajs/dashboard
pnpm add react-router-dom@[exact-version]
```
**npm/yarn Users**: DO NOT install - already available through dashboard dependencies.
## Basic Navigation with Link Component
Use the `Link` component for internal navigation in widgets and custom pages:
```tsx
import { Link } from "react-router-dom"
import { Text } from "@medusajs/ui"
import { TriangleRightMini } from "@medusajs/icons"
// Link to a custom page
<Link
to="/custom/my-page"
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
>
<div className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-3 transition-colors">
<div className="flex items-center gap-3">
<Text size="small" leading="compact" weight="plus">
Go to Custom Page
</Text>
<div className="size-7 flex items-center justify-center">
<TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
</div>
</div>
</div>
</Link>
```
### Link with Dynamic ID
```tsx
// Link to product details
<Link to={`/products/${product.id}`}>
<Text size="small" leading="compact" weight="plus">
{product.title}
</Text>
</Link>
```
### Button-styled Link
```tsx
import { Button } from "@medusajs/ui"
import { Link } from "react-router-dom"
<Button asChild size="small" variant="secondary">
<Link to="/custom/my-page">
View Details
</Link>
</Button>
```
## Programmatic Navigation
Use `useNavigate` for navigation after actions (e.g., after creating an entity):
```tsx
import { useNavigate } from "react-router-dom"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast, Button } from "@medusajs/ui"
import { sdk } from "../lib/client"
const CreateProductWidget = () => {
const navigate = useNavigate()
const queryClient = useQueryClient()
const createProduct = useMutation({
mutationFn: (data) => sdk.admin.product.create(data),
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ["products"] })
toast.success("Product created successfully")
// Navigate to the new product's page
navigate(`/products/${result.product.id}`)
},
onError: (error) => {
toast.error(error.message || "Failed to create product")
},
})
const handleCreate = () => {
createProduct.mutate({
title: "New Product",
// ... other fields
})
}
return (
<Button onClick={handleCreate} isLoading={createProduct.isPending}>
Create and View Product
</Button>
)
}
```
### Navigate with State
Pass data to the destination page:
```tsx
navigate("/custom/review", {
state: { productId: product.id, productTitle: product.title }
})
// Access in destination page
import { useLocation } from "react-router-dom"
const ReviewPage = () => {
const location = useLocation()
const { productId, productTitle } = location.state || {}
return <div>Reviewing: {productTitle}</div>
}
```
### Navigate Back
```tsx
const navigate = useNavigate()
<Button onClick={() => navigate(-1)}>
Go Back
</Button>
```
## Accessing Route Parameters
In custom pages, access URL parameters with `useParams`:
```tsx
// Custom page at: /custom/products/:id
import { useParams } from "react-router-dom"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/client"
import { Container, Heading } from "@medusajs/ui"
const ProductDetailsPage = () => {
const { id } = useParams() // Get :id from URL
const { data: product, isLoading } = useQuery({
queryFn: () => sdk.admin.product.retrieve(id, {
fields: "+metadata,+variants.*",
}),
queryKey: ["product", id],
enabled: !!id, // Only fetch if ID exists
})
if (isLoading) return <div>Loading...</div>
return (
<Container>
<Heading>{product?.title}</Heading>
{/* Product details */}
</Container>
)
}
export default ProductDetailsPage
```
### Multiple Parameters
```tsx
// Route: /custom/orders/:orderId/items/:itemId
const { orderId, itemId } = useParams()
const { data } = useQuery({
queryFn: () => sdk.client.fetch(`/admin/orders/${orderId}/items/${itemId}`),
queryKey: ["order-item", orderId, itemId],
enabled: !!orderId && !!itemId,
})
```
### Query Parameters
Use `useSearchParams` for query string parameters:
```tsx
import { useSearchParams } from "react-router-dom"
const ProductsPage = () => {
const [searchParams, setSearchParams] = useSearchParams()
const status = searchParams.get("status") // Get ?status=published
const page = searchParams.get("page") // Get ?page=2
const { data } = useQuery({
queryFn: () => sdk.admin.product.list({
status,
offset: (parseInt(page) || 0) * 15,
}),
queryKey: ["products", status, page],
})
// Update query params
const handleFilterChange = (newStatus: string) => {
setSearchParams({ status: newStatus, page: "0" })
}
return (
<div>
<Button onClick={() => handleFilterChange("published")}>
Published Only
</Button>
{/* Products list */}
</div>
)
}
```
## Linking to Built-in Admin Pages
Link to standard Medusa admin pages:
```tsx
import { Link } from "react-router-dom"
// Product details
<Link to={`/products/${productId}`}>View Product</Link>
// Order details
<Link to={`/orders/${orderId}`}>View Order</Link>
// Customer details
<Link to={`/customers/${customerId}`}>View Customer</Link>
// Product categories
<Link to="/categories">View Categories</Link>
// Settings
<Link to="/settings">Settings</Link>
// Custom field in settings
<Link to="/settings/custom-field-name">Custom Settings</Link>
```
### Common Built-in Routes
```tsx
const ADMIN_ROUTES = {
products: "/products",
productDetails: (id: string) => `/products/${id}`,
orders: "/orders",
orderDetails: (id: string) => `/orders/${id}`,
customers: "/customers",
customerDetails: (id: string) => `/customers/${id}`,
categories: "/categories",
inventory: "/inventory",
pricing: "/pricing",
settings: "/settings",
}
// Usage
<Link to={ADMIN_ROUTES.productDetails(product.id)}>
View Product
</Link>
```
## Navigation from Widgets
### Pattern: View All Link
Add a "View All" link from a widget to a custom page:
```tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Button, Text } from "@medusajs/ui"
import { Link } from "react-router-dom"
import { DetailWidgetProps } from "@medusajs/framework/types"
const RelatedProductsWidget = ({ data: product }) => {
// ... widget logic
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Related Products</Heading>
<div className="flex items-center gap-x-2">
<Button size="small" variant="secondary" onClick={() => setOpen(true)}>
Edit
</Button>
<Button asChild size="small" variant="transparent">
<Link to={`/custom/products/${product.id}/related`}>
View All
</Link>
</Button>
</div>
</div>
{/* Widget content */}
</Container>
)
}
export const config = defineWidgetConfig({
zone: "product.details.after",
})
export default RelatedProductsWidget
```
### Pattern: List Item Navigation
Make list items clickable to navigate:
```tsx
import { Thumbnail, Text } from "@medusajs/ui"
import { TriangleRightMini } from "@medusajs/icons"
import { Link } from "react-router-dom"
const ProductListItem = ({ product }) => {
return (
<Link
to={`/products/${product.id}`}
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
>
<div className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-2 transition-colors">
<div className="flex items-center gap-3">
<Thumbnail src={product.thumbnail} />
<div className="flex flex-1 flex-col">
<Text size="small" leading="compact" weight="plus">
{product.title}
</Text>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{product.status}
</Text>
</div>
<div className="size-7 flex items-center justify-center">
<TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
</div>
</div>
</div>
</Link>
)
}
```
## Common Navigation Patterns
### Pattern: Back to List
Navigate back to list after viewing details:
```tsx
import { useNavigate } from "react-router-dom"
import { ArrowLeft } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"
const DetailsPage = () => {
const navigate = useNavigate()
return (
<div>
<div className="flex items-center gap-x-2 mb-4">
<IconButton onClick={() => navigate("/custom/products")}>
<ArrowLeft />
</IconButton>
<Heading>Product Details</Heading>
</div>
{/* Details content */}
</div>
)
}
```
### Pattern: Breadcrumb Navigation
```tsx
import { Link } from "react-router-dom"
import { Text } from "@medusajs/ui"
const Breadcrumbs = ({ product }) => {
return (
<div className="flex items-center gap-x-2">
<Link to="/products">
<Text size="small" className="text-ui-fg-subtle hover:text-ui-fg-base">
Products
</Text>
</Link>
<Text size="small" className="text-ui-fg-muted">/</Text>
<Link to={`/products/${product.id}`}>
<Text size="small" className="text-ui-fg-subtle hover:text-ui-fg-base">
{product.title}
</Text>
</Link>
<Text size="small" className="text-ui-fg-muted">/</Text>
<Text size="small" weight="plus">Details</Text>
</div>
)
}
```
### Pattern: Tab Navigation
Navigate between different views using tabs:
```tsx
import { useSearchParams, Link } from "react-router-dom"
import { Tabs } from "@medusajs/ui"
const ProductTabs = () => {
const [searchParams] = useSearchParams()
const activeTab = searchParams.get("tab") || "details"
return (
<Tabs value={activeTab}>
<Tabs.List>
<Tabs.Trigger value="details" asChild>
<Link to="?tab=details">Details</Link>
</Tabs.Trigger>
<Tabs.Trigger value="variants" asChild>
<Link to="?tab=variants">Variants</Link>
</Tabs.Trigger>
<Tabs.Trigger value="media" asChild>
<Link to="?tab=media">Media</Link>
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="details">
{/* Details content */}
</Tabs.Content>
<Tabs.Content value="variants">
{/* Variants content */}
</Tabs.Content>
<Tabs.Content value="media">
{/* Media content */}
</Tabs.Content>
</Tabs>
)
}
```
### Pattern: Action with Navigation
Perform an action then navigate:
```tsx
const deleteProduct = useMutation({
mutationFn: (id) => sdk.admin.product.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
toast.success("Product deleted")
navigate("/products") // Navigate to list after deletion
},
})
```
### Pattern: Conditional Navigation
Navigate based on state or data:
```tsx
const handleComplete = () => {
if (hasErrors) {
toast.error("Please fix errors first")
return
}
if (isDraft) {
navigate(`/custom/products/${id}/publish`)
} else {
navigate("/products")
}
}
```
## Important Notes
1. **pnpm users**: Must install `react-router-dom` with exact version from dashboard
2. **npm/yarn users**: Do NOT install `react-router-dom` - already available
3. **Always use relative paths** starting with `/` for internal navigation
4. **Use Link for navigation links** - better for SEO and accessibility
5. **Use navigate for programmatic navigation** - after actions or based on logic
6. **Always handle loading states** when fetching route parameter-based data
7. **Clean up on unmount** when using listeners or subscriptions in routes
8. **Maintain focus management** for accessibility when navigating

View File

@@ -0,0 +1,407 @@
# Table Selection Pattern
## Contents
- [Pre-Implementation Requirements for pnpm](#pre-implementation-requirements-for-pnpm)
- [Complete Widget Example: Related Products Selection](#complete-widget-example-related-products-selection)
- [Key Implementation Details](#key-implementation-details)
- [Pattern Variations](#pattern-variations)
- [Important Notes](#important-notes)
This is a complete reference implementation for selecting from large datasets (Products, Categories, Regions, etc.) in Medusa Admin customizations. This pattern uses FocusModal with DataTable for an optimal UX.
## Pre-Implementation Requirements for pnpm
**⚠️ pnpm Users**: This example requires the following packages. Install them BEFORE implementing:
- `@tanstack/react-query` - for useQuery and useMutation
- `react-router-dom` - for Link component (optional, can be removed if not needed)
Check and install with exact versions:
```bash
pnpm list @tanstack/react-query --depth=10 | grep @medusajs/dashboard
pnpm add @tanstack/react-query@[exact-version]
pnpm list react-router-dom --depth=10 | grep @medusajs/dashboard
pnpm add react-router-dom@[exact-version]
```
See the main SKILL.md for full pnpm setup instructions.
## Complete Widget Example: Related Products Selection
```tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import {
Container,
Heading,
Button,
toast,
FocusModal,
Text,
DataTable,
DataTableRowSelectionState,
DataTablePaginationState,
createDataTableColumnHelper,
useDataTable,
} from "@medusajs/ui"
import { HttpTypes } from "@medusajs/types"
import { useMemo, useState } from "react"
import { DetailWidgetProps } from "@medusajs/framework/types"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/client"
import { PencilSquare } from "@medusajs/icons"
const ProductRelatedProductsWidget = ({
data: product
}: DetailWidgetProps<HttpTypes.AdminProduct>) => {
const [open, setOpen] = useState(false)
const queryClient = useQueryClient()
// Parse existing related products from metadata
const initialIds = useMemo(() => {
if (product?.metadata?.related_product_ids) {
try {
const ids = JSON.parse(product.metadata.related_product_ids as string)
return Array.isArray(ids) ? ids : []
} catch {
return []
}
}
return []
}, [product?.metadata?.related_product_ids])
// Initialize selection state with existing related products
const initialState = useMemo(() => {
return initialIds.reduce((acc, id) => {
acc[id] = true
return acc
}, {} as DataTableRowSelectionState)
}, [initialIds])
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(initialState)
const [searchValue, setSearchValue] = useState("")
const [pagination, setPagination] = useState<DataTablePaginationState>({
pageIndex: 0,
pageSize: 10
})
// IMPORTANT: Separate queries for display and modal selection
// Query 1: Fetch selected products for display (loads on mount)
const { data: displayProducts } = useQuery({
queryFn: async () => {
if (initialIds.length === 0) return { products: [] }
// Fetch specific products by IDs for display
const response = await sdk.admin.product.list({
id: initialIds, // Fetch only the selected products
limit: initialIds.length,
})
return response
},
queryKey: ["related-products-display", initialIds],
enabled: initialIds.length > 0, // Only fetch if there are IDs
})
// Query 2: Fetch products for modal selection (only when modal is open)
const limit = pagination.pageSize
const offset = pagination.pageIndex * limit
const { data: modalProducts, isLoading } = useQuery({
queryFn: () => sdk.admin.product.list({
limit,
offset,
q: searchValue || undefined,
}),
queryKey: ["products-selection", limit, offset, searchValue],
keepPreviousData: true, // Smooth pagination
enabled: open, // Only load when modal is open
})
// Mutation to update the product metadata
const updateProduct = useMutation({
mutationFn: (relatedProductIds: string[]) => {
return sdk.admin.product.update(product.id, {
metadata: {
...product.metadata,
related_product_ids: JSON.stringify(relatedProductIds),
},
})
},
onSuccess: () => {
// Invalidate queries to refresh the display
queryClient.invalidateQueries({ queryKey: ["product", product.id] })
queryClient.invalidateQueries({ queryKey: ["related-products-display"] })
toast.success("Success", {
description: "Related products updated successfully",
dismissLabel: "Close",
})
setOpen(false)
},
onError: (error) => {
console.error("Error saving related products:", error)
toast.error("Error", {
description: "Failed to update related products",
dismissLabel: "Close",
})
},
})
// Get selected product IDs
const selectedProductIds = useMemo(() => Object.keys(rowSelection), [rowSelection])
// Use display query for showing selected products
const selectedProducts = displayProducts?.products || []
const handleSubmit = () => {
updateProduct.mutate(selectedProductIds)
}
const columns = useColumns()
// Use modalProducts for the selection table
const availableProducts = useMemo(() => {
if (!modalProducts?.products) return []
return modalProducts.products.filter(p => p.id !== product.id)
}, [modalProducts?.products, product.id])
const table = useDataTable({
data: availableProducts,
columns,
getRowId: (row) => row.id,
rowCount: modalProducts?.count || 0,
isLoading,
rowSelection: {
state: rowSelection,
onRowSelectionChange: setRowSelection,
},
search: {
state: searchValue,
onSearchChange: setSearchValue,
},
pagination: {
state: pagination,
onPaginationChange: setPagination,
},
})
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Related Products</Heading>
<Button
size="small"
variant="secondary"
onClick={() => setOpen(true)}
>
<PencilSquare />
</Button>
</div>
<div className="px-6 py-4">
{selectedProducts.length === 0 ? (
<Text size="small" leading="compact" className="text-ui-fg-subtle">
No related products selected
</Text>
) : (
<div className="flex flex-col gap-y-2">
{selectedProducts.map((p) => (
<div key={p.id} className="flex items-center justify-between">
<Text size="small" leading="compact" weight="plus">
{p.title}
</Text>
</div>
))}
</div>
)}
</div>
<FocusModal open={open} onOpenChange={setOpen}>
<FocusModal.Content>
<div className="flex h-full flex-col overflow-hidden">
<FocusModal.Header />
<FocusModal.Body className="flex items-start justify-center">
<div className="w-full max-w-3xl">
<div className="flex flex-col gap-y-6">
<DataTable instance={table}>
<DataTable.Toolbar>
<div className="flex gap-2">
<DataTable.Search placeholder="Search products..." />
</div>
</DataTable.Toolbar>
<DataTable.Table />
<DataTable.Pagination />
</DataTable>
</div>
</div>
</FocusModal.Body>
<FocusModal.Footer>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<Button size="small" variant="secondary">
Cancel
</Button>
</FocusModal.Close>
<Button
size="small"
onClick={handleSubmit}
isLoading={updateProduct.isPending}
>
Save
</Button>
</div>
</FocusModal.Footer>
</div>
</FocusModal.Content>
</FocusModal>
</Container>
)
}
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>()
const useColumns = () => {
return useMemo(() => [
columnHelper.select(),
columnHelper.accessor("title", {
header: "Title",
}),
columnHelper.accessor("status", {
header: "Status",
}),
columnHelper.accessor("created_at", {
header: "Created",
cell: ({ getValue }) => new Date(getValue()).toLocaleDateString(),
}),
], [])
}
export const config = defineWidgetConfig({
zone: "product.details.after",
})
export default ProductRelatedProductsWidget
```
## Key Implementation Details
### 1. Typography
- Use `<Text size="small" leading="compact" weight="plus">` for product titles and labels
- Use `<Text size="small" leading="compact" className="text-ui-fg-subtle">` for descriptions
- Container headers can use `<Heading>` component
- See [typography.md](typography.md) for complete typography guidelines
### 2. State Management
- Use `DataTableRowSelectionState` for tracking selected rows
- Initialize with existing selections from metadata
- Use `DataTablePaginationState` with both `pageIndex` and `pageSize`
### 3. Data Loading Pattern - CRITICAL
**Always use separate queries for display vs modal selection:**
```tsx
// Display query - loads on mount, fetches specific items
const { data: displayProducts } = useQuery({
queryFn: () => sdk.admin.product.list({
id: initialIds, // Fetch specific products by IDs
}),
queryKey: ["related-products-display", initialIds],
enabled: initialIds.length > 0,
})
// Modal query - loads when modal opens, paginated
const { data: modalProducts } = useQuery({
queryFn: () => sdk.admin.product.list({
limit, offset, q: searchValue,
}),
queryKey: ["products-selection", limit, offset, searchValue],
enabled: open, // Only when modal is open
keepPreviousData: true,
})
```
**Why this pattern?**
- Display data loads immediately on mount
- Modal data only loads when needed
- Prevents "No data" on page refresh
- Handles ID-based references properly
### 4. Updating with useMutation and Cache Invalidation
```tsx
const updateProduct = useMutation({
mutationFn: (payload) => sdk.admin.product.update(id, payload),
onSuccess: () => {
// Invalidate BOTH the product and display queries
queryClient.invalidateQueries({ queryKey: ["product", id] })
queryClient.invalidateQueries({ queryKey: ["related-products-display"] })
toast.success("Updated successfully")
// Note: No need to invalidate ["products-selection"] - that's modal data
},
})
```
### 5. DataTable Configuration
Always provide all required configurations:
```tsx
const table = useDataTable({
data: data?.products || [],
columns,
getRowId: (row) => row.id,
rowCount: data?.count || 0,
isLoading,
rowSelection: { /* config */ },
search: { /* config */ },
pagination: { /* config */ },
})
```
### 6. Component Structure
```tsx
<DataTable instance={table}>
<DataTable.Toolbar>
<div className="flex gap-2">
<DataTable.Search placeholder="Search..." />
</div>
</DataTable.Toolbar>
<DataTable.Table />
<DataTable.Pagination />
</DataTable>
```
## Pattern Variations
### For Categories Selection
```tsx
const { data, isLoading } = useQuery({
queryFn: () => sdk.admin.productCategory.list({ limit, offset }),
queryKey: ["categories", limit, offset],
})
```
### For Regions Selection
```tsx
const { data, isLoading } = useQuery({
queryFn: () => sdk.admin.region.list({ limit, offset }),
queryKey: ["regions", limit, offset],
})
```
### For Custom Endpoints
```tsx
const { data, isLoading } = useQuery({
queryFn: () => sdk.client.fetch("/admin/custom-endpoint", {
query: { limit, offset, search: searchValue }
}),
queryKey: ["custom-data", limit, offset, searchValue],
})
```
## Important Notes
1. **Package Manager Considerations**:
- **pnpm users**: MUST install `@tanstack/react-query` and `react-router-dom` BEFORE implementing (see Pre-Implementation Requirements above)
- **npm/yarn users**: DO NOT install these packages - they're already available through dashboard
2. **Always use keepPreviousData: true** for pagination to prevent UI flicker
3. **Search is server-side** - Pass the search value in the query function
4. **Metadata updates replace the entire object** - Spread existing metadata when updating
5. **Use proper query key dependencies** - Include all parameters that affect the data
This pattern provides a consistent, performant way to handle selection from large datasets in Medusa Admin customizations.

View File

@@ -0,0 +1,210 @@
# Typography Guidelines
## Contents
- [Core Typography Pattern](#core-typography-pattern)
- [Typography Rules](#typography-rules)
- [Complete Examples](#complete-examples)
- [Text Color Classes](#text-color-classes)
- [Common Patterns](#common-patterns)
- [Quick Reference](#quick-reference)
## Core Typography Pattern
Use the `Text` component from `@medusajs/ui` for all text elements. Follow these specific patterns:
### Headings/Labels
Use this pattern for section headings, field labels, or any primary text:
```tsx
<Text size="small" leading="compact" weight="plus">
{labelText}
</Text>
```
### Body/Descriptions
Use this pattern for descriptions, helper text, or secondary information:
```tsx
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{descriptionText}
</Text>
```
## Typography Rules
- **Never use** `<Heading>` component for small sections within widgets/containers
- **Always use** `size="small"` and `leading="compact"` for consistency
- **Use** `weight="plus"` for labels and headings
- **Use** `className="text-ui-fg-subtle"` for secondary/descriptive text
- **For larger headings** (page titles, container headers), use the `<Heading>` component
## Complete Examples
### Widget Section with Label and Description
```tsx
import { Text } from "@medusajs/ui"
// In a container or widget:
<div className="flex flex-col gap-y-2">
<Text size="small" leading="compact" weight="plus">
Product Settings
</Text>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
Configure how this product appears in your store
</Text>
</div>
```
### List Item with Title and Subtitle
```tsx
<div className="flex flex-col gap-y-1">
<Text size="small" leading="compact" weight="plus">
Premium T-Shirt
</Text>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
Size: Large Color: Blue
</Text>
</div>
```
### Container Header (Use Heading)
```tsx
import { Container, Heading } from "@medusajs/ui"
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Related Products</Heading>
</div>
{/* ... */}
</Container>
```
### Empty State Message
```tsx
<Text size="small" leading="compact" className="text-ui-fg-subtle">
No related products selected
</Text>
```
### Form Field Label
```tsx
<div className="flex flex-col gap-y-2">
<Text size="small" leading="compact" weight="plus">
Display Name
</Text>
<Input {...props} />
</div>
```
### Error Message
```tsx
<Text size="small" className="text-ui-fg-error">
This field is required
</Text>
```
### Badge or Status Text
```tsx
<div className="flex items-center gap-x-2">
<Text size="small" leading="compact" weight="plus">
Status:
</Text>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
Active
</Text>
</div>
```
## Text Color Classes
Medusa UI provides semantic color classes:
- `text-ui-fg-base` - Default text color (rarely needed, it's the default)
- `text-ui-fg-subtle` - Secondary/muted text
- `text-ui-fg-muted` - Even more muted
- `text-ui-fg-disabled` - Disabled state
- `text-ui-fg-error` - Error messages
- `text-ui-fg-success` - Success messages
- `text-ui-fg-warning` - Warning messages
## Common Patterns
### Two-Column Layout
```tsx
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-y-1">
<Text size="small" leading="compact" className="text-ui-fg-subtle">
Category
</Text>
<Text size="small" leading="compact" weight="plus">
Clothing
</Text>
</div>
<div className="flex flex-col gap-y-1">
<Text size="small" leading="compact" className="text-ui-fg-subtle">
Status
</Text>
<Text size="small" leading="compact" weight="plus">
Published
</Text>
</div>
</div>
```
### Inline Label-Value Pair
```tsx
<div className="flex items-center gap-x-2">
<Text size="small" leading="compact" className="text-ui-fg-subtle">
SKU:
</Text>
<Text size="small" leading="compact" weight="plus">
SHIRT-001
</Text>
</div>
```
### Card with Title and Metadata
```tsx
<div className="flex flex-col gap-y-2">
<Text size="small" leading="compact" weight="plus">
Premium Cotton T-Shirt
</Text>
<div className="flex items-center gap-x-2 text-ui-fg-subtle">
<Text size="small" leading="compact">
$29.99
</Text>
<Text size="small" leading="compact">
</Text>
<Text size="small" leading="compact">
In stock
</Text>
</div>
</div>
```
## Quick Reference
| Use Case | Pattern |
|----------|---------|
| Section headings | `weight="plus"` |
| Primary text | `weight="plus"` |
| Labels | `weight="plus"` |
| Descriptions | `className="text-ui-fg-subtle"` |
| Helper text | `className="text-ui-fg-subtle"` |
| Metadata | `className="text-ui-fg-subtle"` |
| Errors | `className="text-ui-fg-error"` |
| Empty states | `className="text-ui-fg-subtle"` |
| Large headers | `<Heading>` component |

View File

@@ -0,0 +1,178 @@
---
name: building-storefronts
description: Load automatically when planning, researching, or implementing Medusa storefront features (calling custom API routes, SDK integration, React Query patterns, data fetching). REQUIRED for all storefront development in ALL modes (planning, implementation, exploration). Contains SDK usage patterns, frontend integration, and critical rules for calling Medusa APIs.
---
# Medusa Storefront Development
Frontend integration guide for building storefronts with Medusa. Covers SDK usage, React Query patterns, and calling custom API routes.
## When to Apply
**Load this skill for ANY storefront development task, including:**
- Calling custom Medusa API routes from the storefront
- Integrating Medusa SDK in frontend applications
- Using React Query for data fetching
- Implementing mutations with optimistic updates
- Error handling and cache invalidation
**Also load building-with-medusa when:** Building the backend API routes that the storefront calls
## CRITICAL: Load Reference Files When Needed
**The quick reference below is NOT sufficient for implementation.** You MUST load the reference file before writing storefront integration code.
**Load this reference when implementing storefront features:**
- **Calling API routes?** → MUST load `references/frontend-integration.md` first
- **Using SDK?** → MUST load `references/frontend-integration.md` first
- **Implementing React Query?** → MUST load `references/frontend-integration.md` first
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | SDK Usage | CRITICAL | `sdk-` |
| 2 | React Query Patterns | HIGH | `query-` |
| 3 | Data Display | HIGH (includes CRITICAL price rule) | `display-` |
| 4 | Error Handling | MEDIUM | `error-` |
## Quick Reference
### 1. SDK Usage (CRITICAL)
- `sdk-always-use` - **ALWAYS use the Medusa JS SDK for ALL API requests** - NEVER use regular fetch()
- `sdk-existing-methods` - For built-in endpoints, use existing SDK methods (`sdk.store.product.list()`, `sdk.admin.order.retrieve()`)
- `sdk-client-fetch` - For custom API routes, use `sdk.client.fetch()`
- `sdk-required-headers` - SDK automatically adds required headers (publishable API key for store, auth for admin) - regular fetch() missing these headers causes errors
- `sdk-no-json-stringify` - **NEVER use JSON.stringify() on body** - SDK handles serialization automatically
- `sdk-plain-objects` - Pass plain JavaScript objects to body, not strings
- `sdk-locate-first` - Always locate where SDK is instantiated in the project before using it
### 2. React Query Patterns (HIGH)
- `query-use-query` - Use `useQuery` for GET requests (data fetching)
- `query-use-mutation` - Use `useMutation` for POST/DELETE requests (mutations)
- `query-invalidate` - Invalidate queries in `onSuccess` to refresh data after mutations
- `query-keys-hierarchical` - Structure query keys hierarchically for effective cache management
- `query-loading-states` - Always handle `isLoading`, `isPending`, `isError` states
### 3. Data Display (HIGH)
- `display-price-format` - **CRITICAL**: Prices from Medusa are stored as-is ($49.99 = 49.99, NOT in cents). Display them directly - NEVER divide by 100
### 4. Error Handling (MEDIUM)
- `error-on-error` - Implement `onError` callback in mutations to handle failures
- `error-display` - Show error messages to users when mutations fail
- `error-rollback` - Use optimistic updates with rollback on error for better UX
## Critical SDK Pattern
**ALWAYS pass plain objects to the SDK - NEVER use JSON.stringify():**
```typescript
// ✅ CORRECT - Plain object
await sdk.client.fetch("/store/reviews", {
method: "POST",
body: {
product_id: "prod_123",
rating: 5,
}
})
// ❌ WRONG - JSON.stringify breaks the request
await sdk.client.fetch("/store/reviews", {
method: "POST",
body: JSON.stringify({ // ❌ DON'T DO THIS!
product_id: "prod_123",
rating: 5,
})
})
```
**Why this matters:**
- The SDK handles JSON serialization automatically
- Using JSON.stringify() will double-serialize and break the request
- The server won't be able to parse the body
## Common Mistakes Checklist
Before implementing, verify you're NOT doing these:
**SDK Usage:**
- [ ] Using regular fetch() instead of the Medusa JS SDK (causes missing header errors)
- [ ] Not using existing SDK methods for built-in endpoints (e.g., using sdk.client.fetch("/store/products") instead of sdk.store.product.list())
- [ ] Using JSON.stringify() on the body parameter
- [ ] Manually setting Content-Type headers (SDK adds them)
- [ ] Hardcoding SDK import paths (locate in project first)
- [ ] Not using sdk.client.fetch() for custom routes
**React Query:**
- [ ] Not invalidating queries after mutations
- [ ] Using flat query keys instead of hierarchical
- [ ] Not handling loading and error states
- [ ] Forgetting to disable buttons during mutations (isPending)
**Data Display:**
- [ ] **CRITICAL**: Dividing prices by 100 when displaying (prices are stored as-is: $49.99 = 49.99, NOT in cents)
**Error Handling:**
- [ ] Not implementing onError callbacks
- [ ] Not showing error messages to users
- [ ] Not handling network failures gracefully
## How to Use
**For detailed patterns and examples, load reference file:**
```
references/frontend-integration.md - SDK usage, React Query patterns, API integration
```
The reference file contains:
- Step-by-step SDK integration patterns
- Complete React Query examples
- Correct vs incorrect code examples
- Query key best practices
- Optimistic update patterns
- Error handling strategies
## When to Use MedusaDocs MCP Server
**Use this skill for (PRIMARY SOURCE):**
- How to call custom API routes from storefront
- SDK usage patterns (sdk.client.fetch)
- React Query integration patterns
- Common mistakes and anti-patterns
**Use MedusaDocs MCP server for (SECONDARY SOURCE):**
- Built-in SDK methods (sdk.admin.*, sdk.store.*)
- Official Medusa SDK API reference
- Framework-specific configuration options
**Why skills come first:**
- Skills contain critical patterns like "don't use JSON.stringify" that MCP doesn't emphasize
- Skills show correct vs incorrect patterns; MCP shows what's possible
- Planning requires understanding patterns, not just API reference
## Integration with Backend
**⚠️ CRITICAL: ALWAYS use the Medusa JS SDK - NEVER use regular fetch()**
When building features that span backend and frontend:
1. **Backend (building-with-medusa skill):** Module → Workflow → API Route
2. **Storefront (this skill):** SDK → React Query → UI Components
3. **Connection:**
- Built-in endpoints: Use existing SDK methods (`sdk.store.product.list()`)
- Custom API routes: Use `sdk.client.fetch("/store/my-route")`
- **NEVER use regular fetch()** - missing publishable API key causes errors
**Why the SDK is required:**
- Store routes need `x-publishable-api-key` header
- Admin routes need `Authorization` and session headers
- SDK handles all required headers automatically
- Regular fetch() without headers → authentication/authorization errors
See `building-with-medusa` for backend API route patterns.

View File

@@ -0,0 +1,229 @@
# Frontend SDK Integration
## Contents
- [Frontend SDK Pattern](#frontend-sdk-pattern)
- [Locating the SDK](#locating-the-sdk)
- [Using sdk.client.fetch()](#using-sdkclientfetch)
- [React Query Pattern](#react-query-pattern)
- [Query Key Best Practices](#query-key-best-practices)
- [Error Handling](#error-handling)
- [Optimistic Updates](#optimistic-updates)
This guide covers how to integrate Medusa custom API routes with frontend applications using the Medusa SDK and React Query.
**Note:** API routes are also referred to as "endpoints" - these terms are interchangeable.
## Frontend SDK Pattern
### Locating the SDK
**IMPORTANT:** Never hardcode SDK import paths. Always locate where the SDK is instantiated in the project first.
Look for `@medusajs/js-sdk`
The SDK instance is typically exported as `sdk`:
```typescript
import { sdk } from "[LOCATE IN PROJECT]"
```
### Using sdk.client.fetch()
**⚠️ CRITICAL: ALWAYS use the Medusa JS SDK for ALL API requests - NEVER use regular fetch()**
**Why this is critical:**
- **Store API routes** require the publishable API key in headers
- **Admin API routes** require authentication headers
- **Regular fetch()** without these headers will cause errors
- The SDK automatically handles all required headers for you
**When to use what:**
- **Existing endpoints** (built-in Medusa routes): Use existing SDK methods like `sdk.store.product.list()`, `sdk.admin.order.retrieve()`
- **Custom endpoints** (your custom API routes): Use `sdk.client.fetch()` for custom routes
**⚠️ CRITICAL: The SDK handles JSON serialization automatically. NEVER use JSON.stringify() on the body.**
Call custom API routes using the SDK:
```typescript
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
// ✅ CORRECT - Pass object directly
const result = await sdk.client.fetch("/store/my-route", {
method: "POST",
body: {
email: "user@example.com",
name: "John Doe",
},
})
// ❌ WRONG - Don't use JSON.stringify
const result = await sdk.client.fetch("/store/my-route", {
method: "POST",
body: JSON.stringify({ // ❌ DON'T DO THIS!
email: "user@example.com",
}),
})
```
**Key points:**
- **The SDK handles JSON serialization automatically** - just pass plain objects
- **NEVER use JSON.stringify()** - this will break the request
- No need to set Content-Type headers - SDK adds them
- Session/JWT authentication is handled automatically
- Publishable API key is automatically added
### Built-in Endpoints vs Custom Endpoints
**⚠️ CRITICAL: Use the appropriate SDK method based on endpoint type**
```typescript
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
// ✅ CORRECT - Built-in endpoint: Use existing SDK method
const products = await sdk.store.product.list({
limit: 10,
offset: 0
})
// ✅ CORRECT - Custom endpoint: Use sdk.client.fetch()
const reviews = await sdk.client.fetch("/store/products/prod_123/reviews")
// ❌ WRONG - Using regular fetch for ANY endpoint
const products = await fetch("http://localhost:9000/store/products")
// ❌ Error: Missing publishable API key header!
// ❌ WRONG - Using regular fetch for custom endpoint
const reviews = await fetch("http://localhost:9000/store/products/prod_123/reviews")
// ❌ Error: Missing publishable API key header!
// ❌ WRONG - Using sdk.client.fetch() for built-in endpoint when SDK method exists
const products = await sdk.client.fetch("/store/products")
// ❌ Less type-safe than using sdk.store.product.list()
```
**Why this matters:**
- **Store routes** require `x-publishable-api-key` header - SDK adds it automatically
- **Admin routes** require `Authorization` and session cookie headers - SDK adds them automatically
- **Regular fetch()** doesn't include these headers → API returns authentication/authorization errors
- Using existing SDK methods provides **better type safety** and autocomplete
## React Query Pattern
Use `useQuery` for GET requests and `useMutation` for POST/DELETE:
```typescript
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
function MyComponent({ userId }: { userId: string }) {
const queryClient = useQueryClient()
// GET request - fetching data
const { data, isLoading } = useQuery({
queryKey: ["my-data", userId],
queryFn: () => sdk.client.fetch(`/store/my-route?userId=${userId}`),
enabled: !!userId,
})
// POST request - mutation with cache invalidation
const mutation = useMutation({
mutationFn: (input: { email: string }) =>
sdk.client.fetch("/store/my-route", { method: "POST", body: input }),
onSuccess: () => {
// Invalidate and refetch related queries
queryClient.invalidateQueries({ queryKey: ["my-data"] })
},
})
if (isLoading) return <p>Loading...</p>
return (
<div>
<p>{data?.title}</p>
<button
onClick={() => mutation.mutate({ email: "test@example.com" })}
disabled={mutation.isPending}
>
{mutation.isPending ? "Loading..." : "Submit"}
</button>
{mutation.isError && <p>Error occurred</p>}
</div>
)
}
```
**Key states:** `isLoading`, `isPending`, `isSuccess`, `isError`, `error`
## Query Key Best Practices
Structure query keys for effective cache management:
```typescript
// Good: Hierarchical structure
queryKey: ["products", productId]
queryKey: ["products", "list", { page, filters }]
// Invalidate all product queries
queryClient.invalidateQueries({ queryKey: ["products"] })
// Invalidate specific product
queryClient.invalidateQueries({ queryKey: ["products", productId] })
```
## Error Handling
Handle API errors gracefully:
```typescript
const mutation = useMutation({
mutationFn: (input) => sdk.client.fetch("/store/my-route", {
method: "POST",
body: input
}),
onError: (error) => {
console.error("Mutation failed:", error)
// Show error message to user
},
})
// In component
{mutation.isError && (
<p className="error">
{mutation.error?.message || "An error occurred"}
</p>
)}
```
## Optimistic Updates
Update UI immediately before server confirms:
```typescript
const mutation = useMutation({
mutationFn: (newItem) =>
sdk.client.fetch("/store/items", { method: "POST", body: newItem }),
onMutate: async (newItem) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ["items"] })
// Snapshot previous value
const previousItems = queryClient.getQueryData(["items"])
// Optimistically update
queryClient.setQueryData(["items"], (old) => [...old, newItem])
// Return context with snapshot
return { previousItems }
},
onError: (err, newItem, context) => {
// Rollback on error
queryClient.setQueryData(["items"], context.previousItems)
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ["items"] })
},
})
```

View File

@@ -0,0 +1,376 @@
---
name: building-with-medusa
description: Load automatically when planning, researching, or implementing ANY Medusa backend features (custom modules, API routes, workflows, data models, module links, business logic). REQUIRED for all Medusa backend work in ALL modes (planning, implementation, exploration). Contains architectural patterns, best practices, and critical rules that MCP servers don't provide.
---
# Medusa Backend Development
Comprehensive backend development guide for Medusa applications. Contains patterns across 6 categories covering architecture, type safety, business logic placement, and common pitfalls.
## When to Apply
**Load this skill for ANY backend development task, including:**
- Creating or modifying custom modules and data models
- Implementing workflows for mutations
- Building API routes (store or admin)
- Defining module links between entities
- Writing business logic or validation
- Querying data across modules
- Implementing authentication/authorization
**Also load these skills when:**
- **building-admin-dashboard-customizations:** Building admin UI (widgets, pages, forms)
- **building-storefronts:** Calling backend API routes from storefronts (SDK integration)
## CRITICAL: Load Reference Files When Needed
**The quick reference below is NOT sufficient for implementation.** You MUST load relevant reference files before writing code for that component.
**Load these references based on what you're implementing:**
- **Creating a module?** → MUST load `reference/custom-modules.md` first
- **Creating workflows?** → MUST load `reference/workflows.md` first
- **Creating API routes?** → MUST load `reference/api-routes.md` first
- **Creating module links?** → MUST load `reference/module-links.md` first
- **Querying data?** → MUST load `reference/querying-data.md` first
- **Adding authentication?** → MUST load `reference/authentication.md` first
**Minimum requirement:** Load at least 1-2 reference files relevant to your specific task before implementing.
## Critical Architecture Pattern
**ALWAYS follow this flow - never bypass layers:**
```
Module (data models + CRUD operations)
↓ used by
Workflow (business logic + mutations with rollback)
↓ executed by
API Route (HTTP interface, validation middleware)
↓ called by
Frontend (admin dashboard/storefront via SDK)
```
**Key conventions:**
- Only GET, POST, DELETE methods (never PUT/PATCH)
- Workflows are required for ALL mutations
- Business logic belongs in workflow steps, NOT routes
- Query with `query.graph()` for cross-module data retrieval
- Query with `query.index()` (Index Module) for filtering across separate modules with links
- Module links maintain isolation between modules
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Architecture Violations | CRITICAL | `arch-` |
| 2 | Type Safety | CRITICAL | `type-` |
| 3 | Business Logic Placement | HIGH | `logic-` |
| 4 | Import & Code Organization | HIGH | `import-` |
| 5 | Data Access Patterns | MEDIUM (includes CRITICAL price rule) | `data-` |
| 6 | File Organization | MEDIUM | `file-` |
## Quick Reference
### 1. Architecture Violations (CRITICAL)
- `arch-workflow-required` - Use workflows for ALL mutations, never call module services from routes
- `arch-layer-bypass` - Never bypass layers (route → service without workflow)
- `arch-http-methods` - Use only GET, POST, DELETE (never PUT/PATCH)
- `arch-module-isolation` - Use module links, not direct cross-module service calls
- `arch-query-config-fields` - Don't set explicit `fields` when using `req.queryConfig`
### 2. Type Safety (CRITICAL)
- `type-request-schema` - Pass Zod inferred type to `MedusaRequest<T>` when using `req.validatedBody`
- `type-authenticated-request` - Use `AuthenticatedMedusaRequest` for protected routes (not `MedusaRequest`)
- `type-export-schema` - Export both Zod schema AND inferred type from middlewares
- `type-linkable-auto` - Never add `.linkable()` to data models (automatically added)
- `type-module-name-camelcase` - Module names MUST be camelCase, never use dashes (causes runtime errors)
### 3. Business Logic Placement (HIGH)
- `logic-workflow-validation` - Put business validation in workflow steps, not API routes
- `logic-ownership-checks` - Validate ownership/permissions in workflows, not routes
- `logic-module-service` - Keep modules simple (CRUD only), put logic in workflows
### 4. Import & Code Organization (HIGH)
- `import-top-level` - Import workflows/modules at file top, never use `await import()` in route body
- `import-static-only` - Use static imports for all dependencies
- `import-no-dynamic-routes` - Dynamic imports add overhead and break type checking
### 5. Data Access Patterns (MEDIUM)
- `data-price-format` - **CRITICAL**: Prices are stored as-is in Medusa (49.99 stored as 49.99, NOT in cents). Never multiply by 100 when saving or divide by 100 when displaying
- `data-query-method` - Use `query.graph()` for retrieving data; use `query.index()` (Index Module) for filtering across linked modules
- `data-query-graph` - Use `query.graph()` for cross-module queries with dot notation (without cross-module filtering)
- `data-query-index` - Use `query.index()` when filtering by properties of linked data models in separate modules
- `data-list-and-count` - Use `listAndCount` for single-module paginated queries
- `data-linked-filtering` - `query.graph()` can't filter by linked module fields - use `query.index()` or query from that entity directly
- `data-no-js-filter` - Don't use JavaScript `.filter()` on linked data - use database filters (`query.index()` or query the entity)
- `data-same-module-ok` - Can filter by same-module relations with `query.graph()` (e.g., product.variants)
- `data-auth-middleware` - Trust `authenticate` middleware, don't manually check `req.auth_context`
### 6. File Organization (MEDIUM)
- `file-workflow-steps` - Recommended: Create steps in `src/workflows/steps/[name].ts`
- `file-workflow-composition` - Composition functions in `src/workflows/[name].ts`
- `file-middleware-exports` - Export schemas and types from middleware files
- `file-links-directory` - Define module links in `src/links/[name].ts`
## Workflow Composition Rules
**The workflow function has critical constraints:**
```typescript
// ✅ CORRECT
const myWorkflow = createWorkflow(
"name",
function (input) { // Regular function, not async, not arrow
const result = myStep(input) // No await
return new WorkflowResponse(result)
}
)
// ❌ WRONG
const myWorkflow = createWorkflow(
"name",
async (input) => { // ❌ No async, no arrow functions
const result = await myStep(input) // ❌ No await
if (input.condition) { /* ... */ } // ❌ No conditionals
return new WorkflowResponse(result)
}
)
```
**Constraints:**
- No async/await (runs at load time)
- No arrow functions (use `function`)
- No conditionals/ternaries (use `when()`)
- No variable manipulation (use `transform()`)
- No date creation (use `transform()`)
- Multiple step calls need `.config({ name: "unique-name" })` to avoid conflicts
## Common Mistakes Checklist
Before implementing, verify you're NOT doing these:
**Architecture:**
- [ ] Calling module services directly from API routes
- [ ] Using PUT or PATCH methods
- [ ] Bypassing workflows for mutations
- [ ] Setting `fields` explicitly with `req.queryConfig`
- [ ] Skipping migrations after creating module links
**Type Safety:**
- [ ] Forgetting `MedusaRequest<SchemaType>` type argument
- [ ] Using `MedusaRequest` instead of `AuthenticatedMedusaRequest` for protected routes
- [ ] Not exporting Zod inferred type from middlewares
- [ ] Adding `.linkable()` to data models
- [ ] Using dashes in module names (must be camelCase)
**Business Logic:**
- [ ] Validating business rules in API routes
- [ ] Checking ownership in routes instead of workflows
- [ ] Manually checking `req.auth_context?.actor_id` when middleware already applied
**Imports:**
- [ ] Using `await import()` in route handler bodies
- [ ] Dynamic imports for workflows or modules
**Data Access:**
- [ ] **CRITICAL**: Multiplying prices by 100 when saving or dividing by 100 when displaying (prices are stored as-is: $49.99 = 49.99)
- [ ] Filtering by linked module fields with `query.graph()` (use `query.index()` or query from other side instead)
- [ ] Using JavaScript `.filter()` on linked data (use `query.index()` or query the linked entity directly)
- [ ] Not using `query.graph()` for cross-module data retrieval
- [ ] Using `query.graph()` when you need to filter across separate modules (use `query.index()` instead)
## Validating Implementation
**CRITICAL: Always run the build command after completing implementation to catch type errors and runtime issues.**
### When to Validate
- After implementing any new feature
- After making changes to modules, workflows, or API routes
- Before marking tasks as complete
- Proactively, without waiting for the user to ask
### How to Run Build
Detect the package manager and run the appropriate command:
```bash
npm run build # or pnpm build / yarn build
```
### Handling Build Errors
If the build fails:
1. Read the error messages carefully
2. Fix type errors, import issues, and syntax errors
3. Run the build again to verify the fix
4. Do NOT mark implementation as complete until build succeeds
**Common build errors:**
- Missing imports or exports
- Type mismatches (e.g., missing `MedusaRequest<T>` type argument)
- Incorrect workflow composition (async functions, conditionals)
## Next Steps - Testing Your Implementation
**After successfully implementing a feature, always provide these next steps to the user:**
### 1. Start the Development Server
If the server isn't already running, start it:
```bash
npm run dev # or pnpm dev / yarn dev
```
### 2. Access the Admin Dashboard
Open your browser and navigate to:
- **Admin Dashboard:** http://localhost:9000/app
Log in with your admin credentials to test any admin-related features.
### 3. Test API Routes
If you implemented custom API routes, list them for the user to test:
**Admin Routes (require authentication):**
- `POST http://localhost:9000/admin/[your-route]` - Description of what it does
- `GET http://localhost:9000/admin/[your-route]` - Description of what it does
**Store Routes (public or customer-authenticated):**
- `POST http://localhost:9000/store/[your-route]` - Description of what it does
- `GET http://localhost:9000/store/[your-route]` - Description of what it does
**Testing with cURL example:**
```bash
# Admin route (requires authentication)
curl -X POST http://localhost:9000/admin/reviews/123/approve \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
--cookie "connect.sid=YOUR_SESSION_COOKIE"
# Store route
curl -X POST http://localhost:9000/store/reviews \
-H "Content-Type: application/json" \
-d '{"product_id": "prod_123", "rating": 5, "comment": "Great product!"}'
```
### 4. Additional Testing Steps
Depending on what was implemented, mention:
- **Workflows:** Test mutation operations and verify rollback on errors
- **Subscribers:** Trigger events and check logs for subscriber execution
- **Scheduled jobs:** Wait for job execution or check logs for cron output
### Format for Presenting Next Steps
Always present next steps in a clear, actionable format after implementation:
```markdown
## Implementation Complete
The [feature name] has been successfully implemented. Here's how to test it:
### Start the Development Server
[server start command based on package manager]
### Access the Admin Dashboard
Open http://localhost:9000/app in your browser
### Test the API Routes
I've added the following routes:
**Admin Routes:**
- POST /admin/[route] - [description]
- GET /admin/[route] - [description]
**Store Routes:**
- POST /store/[route] - [description]
### What to Test
1. [Specific test case 1]
2. [Specific test case 2]
3. [Specific test case 3]
```
## How to Use
**For detailed patterns and examples, load reference files:**
```
reference/custom-modules.md - Creating modules with data models
reference/workflows.md - Workflow creation and step patterns
reference/api-routes.md - API route structure and validation
reference/module-links.md - Linking entities across modules
reference/querying-data.md - Query patterns and filtering rules
reference/authentication.md - Protecting routes and accessing users
reference/error-handling.md - MedusaError types and patterns
reference/scheduled-jobs.md - Cron jobs and periodic tasks
reference/subscribers-and-events.md - Event handling
reference/troubleshooting.md - Common errors and solutions
```
Each reference file contains:
- Step-by-step implementation checklists
- Correct vs incorrect code examples
- TypeScript patterns and type safety
- Common pitfalls and solutions
## When to Use This Skill vs MedusaDocs MCP Server
**⚠️ CRITICAL: This skill should be consulted FIRST for planning and implementation.**
**Use this skill for (PRIMARY SOURCE):**
- **Planning** - Understanding how to structure Medusa backend features
- **Architecture** - Module → Workflow → API Route patterns
- **Best practices** - Correct vs incorrect code patterns
- **Critical rules** - What NOT to do (common mistakes and anti-patterns)
- **Implementation patterns** - Step-by-step guides with checklists
**Use MedusaDocs MCP server for (SECONDARY SOURCE):**
- Specific method signatures after you know which method to use
- Built-in module configuration options
- Official type definitions
- Framework-level configuration details
**Why skills come first:**
- Skills contain opinionated guidance and anti-patterns MCP doesn't have
- Skills show architectural patterns needed for planning
- MCP is reference material; skills are prescriptive guidance
## Integration with Frontend Applications
**⚠️ CRITICAL: Frontend applications MUST use the Medusa JS SDK for ALL API requests**
When building features that span backend and frontend:
**For Admin Dashboard:**
1. **Backend (this skill):** Module → Workflow → API Route
2. **Frontend:** Load `building-admin-dashboard-customizations` skill
3. **Connection:**
- Built-in endpoints: Use existing SDK methods (`sdk.admin.product.list()`)
- Custom API routes: Use `sdk.client.fetch("/admin/my-route")`
- **NEVER use regular fetch()** - missing auth headers will cause errors
**For Storefronts:**
1. **Backend (this skill):** Module → Workflow → API Route
2. **Frontend:** Load `building-storefronts` skill
3. **Connection:**
- Built-in endpoints: Use existing SDK methods (`sdk.store.product.list()`)
- Custom API routes: Use `sdk.client.fetch("/store/my-route")`
- **NEVER use regular fetch()** - missing publishable API key will cause errors
**Why the SDK is required:**
- Store routes need `x-publishable-api-key` header
- Admin routes need `Authorization` and session headers
- SDK handles all required headers automatically
- Regular fetch() without headers → authentication/authorization errors
See respective frontend skills for complete integration patterns.

View File

@@ -0,0 +1,873 @@
# Custom API Routes
API routes (also called "endpoints") are the primary way to expose custom functionality to storefronts and admin dashboards.
## Contents
- [Path Conventions](#path-conventions)
- [Middleware Validation](#middleware-validation)
- [Query Parameter Validation](#query-parameter-validation)
- [Request Query Config for List Endpoints](#request-query-config-for-list-endpoints)
- [API Route Structure](#api-route-structure)
- [Error Handling](#error-handling)
- [Protected Routes](#protected-routes)
- [Using Workflows in API Routes](#using-workflows-in-api-routes)
## Path Conventions
### Store API Routes (Storefront)
- **Path prefix**: `/store/<rest-of-path>`
- **Examples**: `/store/newsletter-signup`, `/store/custom-search`
- **Authentication**: SDK automatically includes publishable API key
### Admin API Routes (Dashboard)
- **Path prefix**: `/admin/<rest-of-path>`
- **Examples**: `/admin/custom-reports`, `/admin/bulk-operations`
- **Authentication**: SDK automatically includes auth headers (bearer/session)
**Detailed authentication patterns**: See [authentication.md](authentication.md)
## Middleware Validation
**⚠️ CRITICAL**: Always validate request bodies using Zod schemas and the `validateAndTransformBody` middleware.
### Combining Multiple Middlewares
When you need both authentication AND validation, pass them as an array. **NEVER nest validation inside authenticate:**
```typescript
// ✅ CORRECT - Multiple middlewares in array
export default defineMiddlewares({
routes: [
{
matcher: "/store/products/:id/reviews",
method: "POST",
middlewares: [
authenticate("customer", ["session", "bearer"]),
validateAndTransformBody(CreateReviewSchema)
],
},
],
})
// ❌ WRONG - Don't nest validator inside authenticate
export default defineMiddlewares({
routes: [
{
matcher: "/store/products/:id/reviews",
method: "POST",
middlewares: [authenticate("customer", ["session", "bearer"], {
validator: CreateReviewSchema // This doesn't work!
})],
},
],
})
```
**Middleware order matters:** Put `authenticate` before `validateAndTransformBody` so authentication happens first.
### Step 1: Create Middleware File
```typescript
// api/store/[feature]/middlewares.ts
import { MiddlewareRoute, validateAndTransformBody } from "@medusajs/framework"
import { z } from "zod"
export const CreateMySchema = z.object({
email: z.string().email(),
name: z.string().min(2),
// other fields
})
// Export the inferred type for use in route handlers
export type CreateMySchema = z.infer<typeof CreateMySchema>
export const myMiddlewares: MiddlewareRoute[] = [
{
matcher: "/store/my-route",
method: "POST",
middlewares: [validateAndTransformBody(CreateMySchema)],
},
]
```
### Step 2: Register in api/middlewares.ts
```typescript
// api/middlewares.ts
import { defineMiddlewares } from "@medusajs/framework/http"
import { myMiddlewares } from "./store/[feature]/middlewares"
export default defineMiddlewares({
routes: [...myMiddlewares],
})
```
**⚠️ CRITICAL - Middleware Export Pattern:**
Middlewares are exported as **named arrays**, NOT default exports with config objects:
```typescript
// ✅ CORRECT - Named export of MiddlewareRoute array
// api/store/reviews/middlewares.ts
export const reviewMiddlewares: MiddlewareRoute[] = [
{
matcher: "/store/reviews",
method: "POST",
middlewares: [validateAndTransformBody(CreateReviewSchema)],
},
]
// ✅ CORRECT - Import and spread the named array
// api/middlewares.ts
import { reviewMiddlewares } from "./store/reviews/middlewares"
export default defineMiddlewares({
routes: [...reviewMiddlewares],
})
```
```typescript
// ❌ WRONG - Don't use default export with .config
// api/store/reviews/middlewares.ts
export default {
config: {
routes: [...], // This is NOT the middleware pattern!
},
}
// ❌ WRONG - Don't access .config.routes
// api/middlewares.ts
import reviewMiddlewares from "./store/reviews/middlewares"
export default defineMiddlewares({
routes: [...reviewMiddlewares.config.routes], // This doesn't work!
})
```
**Why this matters:**
- Middleware files export arrays directly, not config objects
- Route files (like `route.ts`) use `export const config = defineRouteConfig(...)`
- Don't confuse the two patterns - middlewares are simpler (just an array)
### Step 3: Use Typed req.validatedBody in Route
**⚠️ CRITICAL**: When using `req.validatedBody`, you MUST pass the inferred Zod schema type as a type argument to `MedusaRequest`. Otherwise, you'll get TypeScript errors when accessing `req.validatedBody`.
```typescript
// api/store/my-route/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { CreateMySchema } from "./middlewares"
// ✅ CORRECT: Pass the Zod schema type as type argument
export async function POST(
req: MedusaRequest<CreateMySchema>,
res: MedusaResponse
) {
// Now req.validatedBody is properly typed
const { email, name } = req.validatedBody
// ... rest of implementation
}
// ❌ WRONG: Without type argument, req.validatedBody will have type errors
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const { email, name } = req.validatedBody // Type error!
}
```
## Query Parameter Validation
For API routes that accept query parameters, use the `validateAndTransformQuery` middleware to validate them.
**⚠️ IMPORTANT**: When using `validateAndTransformQuery`, access query parameters via `req.validatedQuery` instead of `req.query`.
### Step 1: Create Validation Schema
Create a Zod schema for the query parameters. Since query parameters are originally strings or arrays of strings, use `z.preprocess` to transform them to other types:
```typescript
// api/custom/validators.ts
import { z } from "zod"
export const GetMyRouteSchema = z.object({
cart_id: z.string(), // String parameters don't need preprocessing
limit: z.preprocess(
(val) => {
if (val && typeof val === "string") {
return parseInt(val)
}
return val
},
z.number().optional()
),
status: z.enum(["active", "pending", "completed"]).optional(),
})
```
### Step 2: Add Middleware
```typescript
// api/middlewares.ts
import {
validateAndTransformQuery,
defineMiddlewares,
} from "@medusajs/framework/http"
import { GetMyRouteSchema } from "./custom/validators"
export default defineMiddlewares({
routes: [
{
matcher: "/store/my-route",
method: "GET",
middlewares: [
validateAndTransformQuery(GetMyRouteSchema, {}),
],
},
],
})
```
### Step 3: Use Validated Query in Route
```typescript
// api/store/my-route/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export async function GET(req: MedusaRequest, res: MedusaResponse) {
// Access validated query parameters (not req.query!)
const { cart_id, limit, status } = req.validatedQuery
// cart_id is string, limit is number, status is enum
const query = req.scope.resolve("query")
const { data } = await query.graph({
entity: "my_entity",
fields: ["id", "name"],
filters: { cart_id, status },
})
return res.json({ items: data })
}
```
## Request Query Config for List Endpoints
**⚠️ BEST PRACTICE**: For API routes that retrieve lists of resources, use request query config to allow clients to control fields, pagination, and ordering.
This pattern:
- Allows clients to specify which fields/relations to retrieve
- Enables client-controlled pagination
- Supports custom ordering
- Provides sensible defaults
### Step 1: Add Middleware with createFindParams
```typescript
// api/middlewares.ts
import {
validateAndTransformQuery,
defineMiddlewares,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
// createFindParams() generates a schema that accepts:
// - fields: Select specific fields/relations
// - offset: Skip N items
// - limit: Max items to return
// - order: Order by field(s) ASC/DESC
export const GetProductsSchema = createFindParams()
export default defineMiddlewares({
routes: [
{
matcher: "/store/products",
method: "GET",
middlewares: [
validateAndTransformQuery(
GetProductsSchema,
{
defaults: [
"id",
"title",
"variants.*", // Include all variant fields by default
],
isList: true, // Indicates this returns a list
defaultLimit: 15, // Default pagination limit
}
),
],
},
],
})
```
**Configuration Options:**
- `defaults`: Array of default fields and relations to retrieve
- `isList`: Boolean indicating if this returns a list (affects pagination)
- `allowed`: (Optional) Array of fields/relations allowed in the `fields` query param
- `defaultLimit`: (Optional) Default limit if not provided (default: 50)
### Step 2: Use Query Config in Route
**⚠️ CRITICAL**: When using `req.queryConfig`, do NOT explicitly set the `fields` property in your query. The `queryConfig` already contains the fields configuration, and setting it explicitly will cause TypeScript errors.
```typescript
// api/store/products/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
// ✅ CORRECT: Only use ...req.queryConfig (includes fields, pagination, etc.)
const { data: products } = await query.graph({
entity: "product",
...req.queryConfig, // Contains fields, select, limit, offset, order
})
return res.json({ products })
}
// ❌ WRONG: Don't set fields explicitly when using queryConfig
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
const { data: products } = await query.graph({
entity: "product",
fields: ["id", "title"], // ❌ Type error! queryConfig already sets fields
...req.queryConfig,
})
return res.json({ products })
}
```
**If you need additional filters**, only add those - not fields:
```typescript
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
const { id } = req.params
// ✅ CORRECT: Add filters while using queryConfig
const { data: products } = await query.graph({
entity: "product",
filters: { id }, // Additional filters are OK
...req.queryConfig, // Fields come from here
})
return res.json({ products })
}
```
### Step 3: Client Usage Examples
Clients can now control the API response:
```typescript
// Default response (uses middleware defaults)
GET /store/products
// Returns: id, title, variants.*
// Custom fields selection
GET /store/products?fields=id,title,description
// Returns: only id, title, description
// Pagination
GET /store/products?limit=10&offset=20
// Returns: 10 items, skipping first 20
// Ordering
GET /store/products?order=title
// Returns: products ordered by title ascending
GET /store/products?order=-created_at
// Returns: products ordered by created_at descending (- prefix)
// Combined
GET /store/products?fields=id,title,brand.*&limit=5&order=-created_at
// Returns: 5 items with custom fields, newest first
```
### Advanced: Custom Query Param + Query Config
You can combine custom query parameters with query config:
```typescript
// validators.ts
import { z } from "zod"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
export const GetProductsSchema = createFindParams().merge(
z.object({
category_id: z.string().optional(),
in_stock: z.preprocess(
(val) => val === "true",
z.boolean().optional()
),
})
)
```
```typescript
// route.ts
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
const { category_id, in_stock } = req.validatedQuery
const filters: any = {}
if (category_id) filters.category_id = category_id
if (in_stock !== undefined) filters.in_stock = in_stock
const { data: products } = await query.graph({
entity: "product",
filters,
...req.queryConfig, // Still get fields, pagination, order
})
return res.json({ products })
}
```
## Import Organization
**⚠️ CRITICAL**: Always import workflows, modules, and other dependencies at the TOP of the file, never inside the route handler function body.
### ✅ CORRECT - Imports at Top
```typescript
// api/store/reviews/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { createReviewWorkflow } from "../../../workflows/create-review"
import { CreateReviewSchema } from "./middlewares"
export async function POST(
req: MedusaRequest<CreateReviewSchema>,
res: MedusaResponse
) {
const { result } = await createReviewWorkflow(req.scope).run({
input: req.validatedBody
})
return res.json({ review: result })
}
```
### ❌ WRONG - Dynamic Imports in Route Body
```typescript
// api/store/reviews/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export async function POST(req: MedusaRequest, res: MedusaResponse) {
// ❌ WRONG: Don't use dynamic imports in route handlers
const { createReviewWorkflow } = await import("../../../workflows/create-review")
const { result } = await createReviewWorkflow(req.scope).run({
input: req.validatedBody
})
return res.json({ review: result })
}
```
**Why this matters:**
- Dynamic imports add unnecessary overhead to every request
- Makes code harder to read and maintain
- Breaks static analysis and TypeScript checking
- Can cause module resolution issues in production
## API Route Structure
**⚠️ IMPORTANT**: Medusa uses only GET, POST and DELETE as a convention.
- **GET** for reads
- **POST** for mutations (create/update)
- **DELETE** for deletions
Don't use PUT or PATCH.
### Basic API Route
```typescript
// api/store/my-route/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
// Query data
const { data: items } = await query.graph({
entity: "entity_name",
fields: ["id", "name"],
})
return res.status(200).json({ items })
}
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const { field } = req.validatedBody
// Execute workflow (mutations should always use workflows)
const { result } = await myWorkflow(req.scope).run({
input: { field },
})
return res.status(200).json({ result })
}
```
### Accessing Request Data
```typescript
// Validated body (from middleware)
const { email, name } = req.validatedBody
// Query parameters
const { page, limit } = req.query
// Route parameters
const { id } = req.params
// Resolve services
const query = req.scope.resolve("query")
const myService = req.scope.resolve("my-module")
```
## Error Handling
Use `MedusaError` for consistent error responses:
```typescript
import { MedusaError } from "@medusajs/framework/utils"
// Not found
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Resource not found")
// Invalid data
throw new MedusaError(MedusaError.Types.INVALID_DATA, "Invalid input provided")
// Unauthorized
throw new MedusaError(MedusaError.Types.UNAUTHORIZED, "Authentication required")
// Conflict
throw new MedusaError(MedusaError.Types.CONFLICT, "Resource already exists")
// Other types: INVALID_STATE, NOT_ALLOWED, DUPLICATE_ERROR
```
### Error Response Format
Medusa automatically formats errors:
```json
{
"type": "not_found",
"message": "Resource not found"
}
```
## Protected Routes
### Default Protected Routes
All routes under these prefixes are automatically protected:
- `/admin/*` - Requires authenticated admin user
- `/store/customers/me/*` - Requires authenticated customer
### Custom Protected Routes
To protect routes under different prefixes, use the `authenticate` middleware:
```typescript
// api/middlewares.ts
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
// Only allow authenticated admin users
{
matcher: "/custom/admin*",
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
},
// Only allow authenticated customers
{
matcher: "/store/reviews*",
middlewares: [authenticate("customer", ["session", "bearer"])],
},
],
})
```
### Accessing Authenticated User
**⚠️ CRITICAL**: For routes protected with `authenticate` middleware, you MUST use `AuthenticatedMedusaRequest` instead of `MedusaRequest` to avoid type errors when accessing `req.auth_context.actor_id`.
```typescript
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
// ✅ CORRECT - Use AuthenticatedMedusaRequest for protected routes
export async function POST(
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) {
// For admin routes
const userId = req.auth_context.actor_id // Admin user ID
// For customer routes
const customerId = req.auth_context.actor_id // Customer ID
// Your logic here
}
// ❌ WRONG - Don't use MedusaRequest for protected routes
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const userId = req.auth_context.actor_id // Type error!
}
```
**See [authentication.md](authentication.md) for complete authentication patterns.**
## Using Workflows in API Routes
**⚠️ BEST PRACTICE**: Workflows are the standard way to perform mutations (create, update, delete) in Medusa. API routes should execute workflows and return their response.
### Example: Create Workflow
```typescript
import { createCustomersWorkflow } from "@medusajs/medusa/core-flows"
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const { email } = req.validatedBody
const { result } = await createCustomersWorkflow(req.scope).run({
input: {
customersData: [
{
email,
has_account: false,
},
],
},
})
return res.json({ customer: result[0] })
}
```
### Example: Custom Workflow
```typescript
import { myCustomWorkflow } from "../../workflows/my-workflow"
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const { data } = req.validatedBody
try {
const { result } = await myCustomWorkflow(req.scope).run({
input: { data },
})
return res.json({ result })
} catch (error) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
error.message
)
}
}
```
## Common Built-in Workflows
Ask MedusaDocs for specific workflow names and their input parameters:
- Customer workflows: create, update, delete customers
- Product workflows: create, update, delete products
- Order workflows: create, cancel, fulfill orders
- Cart workflows: create, update, complete carts
- And many more...
## API Route Organization
Organize routes by feature or domain:
```
src/api/
├── admin/
│ ├── custom-reports/
│ │ ├── route.ts
│ │ └── middlewares.ts
│ └── bulk-operations/
│ ├── route.ts
│ └── middlewares.ts
└── store/
├── newsletter/
│ ├── route.ts
│ └── middlewares.ts
└── reviews/
├── route.ts
├── [id]/
│ └── route.ts
└── middlewares.ts
```
## Common Patterns
### Pattern: List with Query Config (Recommended)
```typescript
// middlewares.ts
import { validateAndTransformQuery } from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
export const GetMyEntitiesSchema = createFindParams()
export default defineMiddlewares({
routes: [
{
matcher: "/store/my-entities",
method: "GET",
middlewares: [
validateAndTransformQuery(GetMyEntitiesSchema, {
defaults: ["id", "name", "created_at"],
isList: true,
defaultLimit: 15,
}),
],
},
],
})
// route.ts
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
const { data, metadata } = await query.graph({
entity: "my_entity",
...req.queryConfig, // Handles fields, pagination automatically
})
return res.json({
items: data,
count: metadata.count,
limit: req.queryConfig.pagination.take,
offset: req.queryConfig.pagination.skip,
})
}
```
### Pattern: Retrieve Single Resource with Relations
```typescript
// For single resource endpoints, you can still use query config
// middlewares.ts
export const GetMyEntitySchema = createFindParams()
export default defineMiddlewares({
routes: [
{
matcher: "/store/my-entities/:id",
method: "GET",
middlewares: [
validateAndTransformQuery(GetMyEntitySchema, {
defaults: ["id", "name", "variants.*", "brand.*"],
isList: false, // Single resource
}),
],
},
],
})
// route.ts
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
const { id } = req.params
const { data } = await query.graph({
entity: "my_entity",
filters: { id },
...req.queryConfig,
})
if (!data || data.length === 0) {
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Resource not found")
}
return res.json({ item: data[0] })
}
```
### Pattern: Search with Custom Filters + Query Config
```typescript
// validators.ts
export const GetMyEntitiesSchema = createFindParams().merge(
z.object({
q: z.string().optional(), // Search query
status: z.enum(["active", "pending", "completed"]).optional(),
})
)
// middlewares.ts
export default defineMiddlewares({
routes: [
{
matcher: "/store/my-entities",
method: "GET",
middlewares: [
validateAndTransformQuery(GetMyEntitiesSchema, {
defaults: ["id", "name", "status"],
isList: true,
}),
],
},
],
})
// route.ts
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
const { q, status } = req.validatedQuery
const filters: any = {}
if (q) {
filters.name = { $like: `%${q}%` }
}
if (status) {
filters.status = status
}
const { data } = await query.graph({
entity: "my_entity",
filters,
...req.queryConfig, // Client can still control fields, pagination
})
return res.json({ items: data })
}
```
### Pattern: Manual Query (When Query Config Not Needed)
For simple queries where you don't need client-controlled fields/pagination:
```typescript
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const query = req.scope.resolve("query")
const { data } = await query.graph({
entity: "my_entity",
fields: ["id", "name"],
filters: { status: "active" },
pagination: {
take: 10,
skip: 0,
},
})
return res.json({ items: data })
}
```

View File

@@ -0,0 +1,556 @@
# Authentication in Medusa
Authentication in Medusa secures API routes and ensures only authorized users can access protected resources.
## Contents
- [Default Protected Routes](#default-protected-routes)
- [Authentication Methods](#authentication-methods)
- [Custom Protected Routes](#custom-protected-routes)
- [Accessing Authenticated User](#accessing-authenticated-user)
- [Authentication Patterns](#authentication-patterns)
## Default Protected Routes
Medusa automatically protects certain route prefixes:
### Admin Routes (`/admin/*`)
- **Who can access**: Authenticated admin users only
- **Authentication methods**: Session, Bearer token, API key
- **Example**: `/admin/products`, `/admin/custom-reports`
### Customer Routes (`/store/customers/me/*`)
- **Who can access**: Authenticated customers only
- **Authentication methods**: Session, Bearer token
- **Example**: `/store/customers/me/orders`, `/store/customers/me/addresses`
**These routes require no additional configuration** - authentication is handled automatically by Medusa.
## Authentication Methods
### Session Authentication
- Used after login via email/password
- Cookie-based session management
- Automatically handled by Medusa SDK
### Bearer Token (JWT)
- Token-based authentication
- Passed in `Authorization: Bearer <token>` header
- Used by frontend applications
### API Key
- Admin-only authentication method
- Used for server-to-server communication
- Passed in `x-medusa-access-token` header
## Custom Protected Routes
**⚠️ CRITICAL: Only add `authenticate` middleware to routes OUTSIDE the default prefixes.**
Routes with these prefixes are automatically authenticated - **do NOT add middleware:**
- `/admin/*` - Already requires authenticated admin user
- `/store/customers/me/*` - Already requires authenticated customer
```typescript
// ✅ CORRECT - Custom route needs authenticate middleware
export default defineMiddlewares({
routes: [
{
matcher: "/store/reviews*", // Not a default protected prefix
middlewares: [authenticate("customer", ["session", "bearer"])],
},
],
})
// ❌ WRONG - /admin routes are automatically authenticated
export default defineMiddlewares({
routes: [
{
matcher: "/admin/reports*", // Already protected!
middlewares: [authenticate("user", ["session", "bearer"])], // Redundant!
},
],
})
```
To protect custom routes outside the default prefixes, use the `authenticate` middleware.
### Protecting Custom Admin Routes
```typescript
// api/middlewares.ts
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom/admin*",
middlewares: [
authenticate("user", ["session", "bearer", "api-key"])
],
},
],
})
```
**Parameters:**
- First parameter: `"user"` for admin users, `"customer"` for customers
- Second parameter: Array of allowed authentication methods
### Protecting Custom Customer Routes
```typescript
// api/middlewares.ts
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/store/reviews*",
middlewares: [
authenticate("customer", ["session", "bearer"])
],
},
],
})
```
### Multiple Protected Routes
```typescript
// api/middlewares.ts
export default defineMiddlewares({
routes: [
// Protect custom admin routes
{
matcher: "/custom/admin*",
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
},
// Protect custom customer routes
{
matcher: "/store/reviews*",
middlewares: [authenticate("customer", ["session", "bearer"])],
},
// Protect wishlist routes
{
matcher: "/store/wishlists*",
middlewares: [authenticate("customer", ["session", "bearer"])],
},
],
})
```
## Accessing Authenticated User
Once a route is protected with the `authenticate` middleware, you can access the authenticated user's information via `req.auth_context`.
**⚠️ CRITICAL - Type Safety**: For protected routes, you MUST use `AuthenticatedMedusaRequest` instead of `MedusaRequest` to avoid type errors when accessing `req.auth_context.actor_id`.
**⚠️ CRITICAL - Manual Validation**: Do NOT manually validate authentication in your route handlers when using the `authenticate` middleware. The middleware already ensures the user is authenticated - manual checks are redundant and indicate a misunderstanding of how middleware works.
### ✅ CORRECT - Using AuthenticatedMedusaRequest
```typescript
// api/store/reviews/[id]/route.ts
// Middleware already applied: authenticate("customer", ["session", "bearer"])
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { deleteReviewWorkflow } from "../../../../workflows/delete-review"
export async function DELETE(
req: AuthenticatedMedusaRequest, // ✅ Use AuthenticatedMedusaRequest for protected routes
res: MedusaResponse
) {
const { id } = req.params
// ✅ CORRECT: Just use req.auth_context.actor_id directly
// The authenticate middleware guarantees this exists
const customerId = req.auth_context.actor_id // No type error!
// Pass to workflow - let the workflow handle business logic validation
const { result } = await deleteReviewWorkflow(req.scope).run({
input: {
reviewId: id,
customerId, // Workflow will validate if review belongs to customer
},
})
return res.json({ success: true })
}
```
### ❌ WRONG - Using MedusaRequest for Protected Routes
```typescript
// api/store/reviews/[id]/route.ts
// Middleware already applied: authenticate("customer", ["session", "bearer"])
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export async function DELETE(
req: MedusaRequest, // ❌ WRONG: Should use AuthenticatedMedusaRequest
res: MedusaResponse
) {
const { id } = req.params
const customerId = req.auth_context.actor_id // ❌ Type error: auth_context might be undefined
return res.json({ success: true })
}
```
### ❌ WRONG - Manual Authentication Check
```typescript
// api/store/reviews/[id]/route.ts
// Middleware already applied: authenticate("customer", ["session", "bearer"])
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
export async function DELETE(req: MedusaRequest, res: MedusaResponse) {
const { id } = req.params
// ❌ WRONG: Don't manually check if user is authenticated
// The authenticate middleware already did this!
if (!req.auth_context?.actor_id) {
throw new MedusaError(
MedusaError.Types.UNAUTHORIZED,
"You must be authenticated"
)
}
const customerId = req.auth_context.actor_id
// Also wrong: don't validate business logic in routes
// (see workflows.md for why this should be in the workflow)
return res.json({ success: true })
}
```
**Why manual checks are wrong:**
- The `authenticate` middleware already validates authentication
- If authentication failed, the request never reaches your handler
- Manual checks suggest you don't trust or understand the middleware
- Adds unnecessary code and potential bugs
### In Admin Routes
```typescript
// api/admin/custom/route.ts
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export async function GET(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
// Get authenticated admin user ID
const userId = req.auth_context.actor_id
const logger = req.scope.resolve("logger")
logger.info(`Request from admin user: ${userId}`)
// Use userId to filter data or track actions
// ...
return res.json({ success: true })
}
```
### In Customer Routes
```typescript
// api/store/reviews/route.ts
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export async function POST(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
// Get authenticated customer ID
const customerId = req.auth_context.actor_id
const { product_id, rating, comment } = req.validatedBody
// Create review associated with the authenticated customer
const { result } = await createReviewWorkflow(req.scope).run({
input: {
customer_id: customerId, // From authenticated context
product_id,
rating,
comment,
},
})
return res.json({ review: result })
}
```
## Authentication Patterns
### Pattern: User-Specific Data
```typescript
// api/admin/my-reports/route.ts
export async function GET(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
const userId = req.auth_context.actor_id
const query = req.scope.resolve("query")
// Get reports created by this admin user
const { data: reports } = await query.graph({
entity: "report",
fields: ["id", "title", "created_at"],
filters: {
created_by: userId,
},
})
return res.json({ reports })
}
```
### Pattern: Ownership Validation
**⚠️ IMPORTANT**: Ownership validation is business logic and should be done in workflow steps, not API routes. The route should only pass the authenticated user ID to the workflow, and the workflow validates ownership.
```typescript
// api/store/reviews/[id]/route.ts
// ✅ CORRECT - Pass user ID to workflow, let workflow validate ownership
export async function DELETE(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
const customerId = req.auth_context.actor_id
const { id } = req.params
// Pass to workflow - workflow will validate ownership
const { result } = await deleteReviewWorkflow(req.scope).run({
input: {
reviewId: id,
customerId, // Workflow validates this review belongs to this customer
},
})
return res.json({ success: true })
}
// ❌ WRONG - Don't validate ownership in the route
export async function DELETE(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
const customerId = req.auth_context.actor_id
const { id } = req.params
const query = req.scope.resolve("query")
// ❌ WRONG: Don't check ownership in the route
const { data: reviews } = await query.graph({
entity: "review",
fields: ["id", "customer_id"],
filters: { id },
})
if (!reviews || reviews.length === 0) {
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Review not found")
}
if (reviews[0].customer_id !== customerId) {
throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Not your review")
}
// This bypasses workflow validation
await deleteReviewWorkflow(req.scope).run({
input: { id },
})
return res.status(204).send()
}
```
**See [workflows.md](workflows.md#business-logic-and-validation-placement) for the complete pattern of validating ownership in workflow steps.**
### Pattern: Customer Profile Routes
```typescript
// api/store/customers/me/wishlist/route.ts
// Automatically protected because it's under /store/customers/me/*
export async function GET(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
const customerId = req.auth_context.actor_id
const query = req.scope.resolve("query")
// Get customer's wishlist
const { data: wishlists } = await query.graph({
entity: "wishlist",
fields: ["id", "products.*"],
filters: {
customer_id: customerId,
},
})
return res.json({ wishlist: wishlists[0] || null })
}
export async function POST(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
const customerId = req.auth_context.actor_id
const { product_id } = req.validatedBody
// Add product to customer's wishlist
const { result } = await addToWishlistWorkflow(req.scope).run({
input: {
customer_id: customerId,
product_id,
},
})
return res.json({ wishlist: result })
}
```
### Pattern: Admin Action Tracking
```typescript
// api/admin/products/[id]/archive/route.ts
export async function POST(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
const adminUserId = req.auth_context.actor_id
const { id } = req.params
// Archive product and track who did it
const { result } = await archiveProductWorkflow(req.scope).run({
input: {
product_id: id,
archived_by: adminUserId,
archived_at: new Date(),
},
})
const logger = req.scope.resolve("logger")
logger.info(`Product ${id} archived by admin user ${adminUserId}`)
return res.json({ product: result })
}
```
### Pattern: Optional Authentication
Some routes may benefit from authentication but don't require it. Use the `authenticate` middleware with `allowUnauthenticated: true`:
```typescript
// api/middlewares.ts
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/store/products/*/reviews",
middlewares: [
authenticate("customer", ["session", "bearer"], {
allowUnauthenticated: true, // Allows access without authentication
})
],
},
],
})
```
```typescript
// api/store/products/[id]/reviews/route.ts
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const customerId = req.auth_context?.actor_id // May be undefined
const { id } = req.params
const query = req.scope.resolve("query")
// Get all reviews
const { data: reviews } = await query.graph({
entity: "review",
fields: ["id", "rating", "comment", "customer_id"],
filters: {
product_id: id,
},
})
// If authenticated, mark customer's own reviews
if (customerId) {
reviews.forEach(review => {
review.is_own = review.customer_id === customerId
})
}
return res.json({ reviews })
}
```
## Frontend Integration
### Store (Customer) Authentication
When using the Medusa JS SDK in storefronts:
```typescript
// Frontend code
import { sdk } from "./lib/sdk"
// Login
await sdk.auth.login("customer", "emailpass", {
email: "customer@example.com",
password: "password",
})
// SDK automatically includes auth headers in subsequent requests
const { customer } = await sdk.store.customer.retrieve()
// Access protected routes
const { orders } = await sdk.store.customer.listOrders()
```
### Admin Authentication
When using the Medusa JS SDK in admin applications:
```typescript
// Admin frontend code
import { sdk } from "./lib/sdk"
// Login
await sdk.auth.login("user", "emailpass", {
email: "admin@example.com",
password: "password",
})
// SDK automatically includes JWT in Authorization header
const { products } = await sdk.admin.product.list()
```
## Security Best Practices
### 1. Use Actor ID from Context
```typescript
// ✅ GOOD: Uses authenticated context
const customerId = req.auth_context.actor_id
// ❌ BAD: Takes user ID from request
const { customer_id } = req.validatedBody // ❌ Can be spoofed
```
### 2. Appropriate Authentication Methods
```typescript
// ✅ GOOD: Admin routes support all methods
authenticate("user", ["session", "bearer", "api-key"])
// ✅ GOOD: Customer routes use session/bearer only
authenticate("customer", ["session", "bearer"])
// ❌ BAD: Customer routes with API key
authenticate("customer", ["api-key"]) // API keys are for admin only
```
### 3. Don't Expose Sensitive Data
```typescript
// ✅ GOOD: Filters sensitive fields
export async function GET(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
const customerId = req.auth_context.actor_id
const customer = await getCustomer(customerId)
// Remove sensitive data before sending
delete customer.password_hash
delete customer.metadata?.internal_notes
return res.json({ customer })
}
```

View File

@@ -0,0 +1,240 @@
# Custom Modules
## Contents
- [When to Create a Custom Module](#when-to-create-a-custom-module)
- [Module Structure](#module-structure)
- [Creating a Custom Module - Implementation Checklist](#creating-a-custom-module---implementation-checklist)
- [Step 1: Create the Data Model](#step-1-create-the-data-model)
- [Step 2: Create the Service](#step-2-create-the-service)
- [Step 3: Export Module Definition](#step-3-export-module-definition)
- [Step 4: Register in Configuration](#step-4-register-in-configuration)
- [Steps 5-6: Generate and Run Migrations](#steps-5-6-generate-and-run-migrations)
- [Resolving Services from Container](#resolving-services-from-container)
- [Auto-Generated CRUD Methods](#auto-generated-crud-methods)
- [Loaders](#loaders)
A module is a reusable package of functionalities related to a single domain or integration. Modules contain data models (database tables) and a service class that provides methods to manage them.
## When to Create a Custom Module
- **New domain concepts**: Brands, wishlists, reviews, loyalty points
- **Third-party integrations**: ERPs, CMSs, custom services
- **Isolated business logic**: Features that don't fit existing commerce modules
## Module Structure
```
src/modules/blog/
├── models/
│ └── post.ts # Data model definitions
├── service.ts # Main service class
└── index.ts # Module definition export
```
## Creating a Custom Module - Implementation Checklist
**IMPORTANT FOR CLAUDE CODE**: When implementing custom modules, use the TodoWrite tool to track your progress through these steps. This ensures you don't miss any critical steps (especially migrations!) and provides visibility to the user.
Create these tasks in your todo list:
- Create data model in src/modules/[name]/models/
- Create service extending MedusaService
- Export module definition in index.ts
- **CRITICAL: Register module in medusa-config.ts** (do this before using the module)
- **CRITICAL: Generate migrations: npx medusa db:generate [module-name]** (Never skip!)
- **CRITICAL: Run migrations: npx medusa db:migrate** (Never skip!)
- Use module service in API routes/workflows
- **CRITICAL: Run build to validate implementation** (catches type errors and issues)
## Step 1: Create the Data Model
```typescript
// src/modules/blog/models/post.ts
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
id: model.id().primaryKey(),
title: model.text(),
content: model.text().nullable(),
published: model.boolean().default(false),
})
// note models automatically get created_at, updated_at and deleted_at added - don't add these explicitly
export default Post
```
**Data model reference**: See [data-models.md](data-models.md)
## Step 2: Create the Service
```typescript
// src/modules/blog/service.ts
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
Post,
}) {}
export default BlogModuleService
```
The service extends `MedusaService` which auto-generates CRUD methods for each data model.
## Step 3: Export Module Definition
```typescript
// src/modules/blog/index.ts
import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"
export const BLOG_MODULE = "blog"
export default Module(BLOG_MODULE, {
service: BlogModuleService,
})
```
**⚠️ CRITICAL - Module Name Format:**
- Module names MUST be in camelCase
- **NEVER use dashes (kebab-case)** in module names
- ✅ CORRECT: `"blog"`, `"productReview"`, `"orderTracking"`
- ❌ WRONG: `"product-review"`, `"order-tracking"` (will cause runtime errors)
**Example of common mistake:**
```typescript
// ❌ WRONG - dashes will break the module
export const PRODUCT_REVIEW_MODULE = "product-review" // Don't do this!
export default Module("product-review", { service: ProductReviewService })
// ✅ CORRECT - use camelCase
export const PRODUCT_REVIEW_MODULE = "productReview"
export default Module("productReview", { service: ProductReviewService })
```
**Why this matters:** Medusa's internal module resolution uses property access syntax (e.g., `container.resolve("productReview")`), and dashes would break this.
## Step 4: Register in Configuration
**IMPORTANT**: You MUST register the module in the configurations BEFORE using it anywhere or generating migrations.
```typescript
// medusa-config.ts
module.exports = defineConfig({
// ...
modules: [{ resolve: "./src/modules/blog" }],
})
```
## Steps 5-6: Generate and Run Migrations
**⚠️ CRITICAL - DO NOT SKIP**: After creating a module and registering it in medusa-config.ts, you MUST run TWO SEPARATE commands. Without this step, the module's database tables won't exist and you will get runtime errors.
```bash
# Step 5: Generate migrations (creates migration files)
# Command format: npx medusa db:generate <module-name>
npx medusa db:generate blog
# Step 6: Run migrations (applies changes to database)
# This command takes NO arguments
npx medusa db:migrate
```
**⚠️ CRITICAL: These are TWO separate commands:**
- ✅ CORRECT: Run `npx medusa db:generate blog` then `npx medusa db:migrate`
- ❌ WRONG: `npx medusa db:generate blog "create blog module"` (no description parameter!)
- ❌ WRONG: Combining into one command
**Why this matters:**
- Migrations create the database tables for your module's data models
- Without migrations, the module service methods (createPosts, listPosts, etc.) will fail
- You must generate migrations BEFORE running them
- This step is REQUIRED before using the module anywhere in your code
**Common mistake:** Creating a module and immediately trying to use it in a workflow or API route without running migrations first. Always run migrations immediately after registering the module.
## Resolving Services from Container
Access your module service in different contexts:
```typescript
// In API routes
const blogService = req.scope.resolve("blog")
const post = await blogService.createPosts({ title: "Hello World" })
// In workflow steps
const blogService = container.resolve("blog")
const posts = await blogService.listPosts({ published: true })
```
The module name used in `Module("blog", ...)` becomes the container resolution key.
## Auto-Generated CRUD Methods
The service auto-generates methods for each data model:
```typescript
// Create - pass object or array of objects
const post = await blogService.createPosts({ title: "Hello" })
const posts = await blogService.createPosts([
{ title: "One" },
{ title: "Two" },
])
// Retrieve - by ID, with optional select/relations
const post = await blogService.retrievePost("post_123")
const post = await blogService.retrievePost("post_123", {
select: ["id", "title"],
})
// List - with filters and options
const posts = await blogService.listPosts()
const posts = await blogService.listPosts({ published: true })
const posts = await blogService.listPosts(
{ published: true }, // filters
{ take: 20, skip: 0, order: { created_at: "DESC" } } // options
)
// List with count - returns [records, totalCount]
const [posts, count] = await blogService.listAndCountPosts({ published: true })
// Update - by ID or with selector/data pattern
const post = await blogService.updatePosts({ id: "post_123", title: "Updated" })
const posts = await blogService.updatePosts({
selector: { published: false },
data: { published: true },
})
// Delete - by ID, array of IDs, or filter object
await blogService.deletePosts("post_123")
await blogService.deletePosts(["post_123", "post_456"])
await blogService.deletePosts({ published: false })
// Soft delete / restore
await blogService.softDeletePosts("post_123")
await blogService.restorePosts("post_123")
```
## Loaders
Loaders run when the Medusa application starts. Use them to initialize connections, seed data (relevant to the Module), or register resources.
```typescript
// src/modules/blog/loaders/hello-world.ts
import { LoaderOptions } from "@medusajs/framework/types"
export default async function helloWorldLoader({ container }: LoaderOptions) {
const logger = container.resolve("logger")
logger.info("[BLOG MODULE] Started!")
}
// Export in module definition (src/modules/blog/index.ts)
import helloWorldLoader from "./loaders/hello-world"
export default Module("blog", {
service: BlogModuleService,
loaders: [helloWorldLoader],
})
```

View File

@@ -0,0 +1,103 @@
# Data Models
Data models represent tables in the database. Use Medusa's Data Model Language (DML) to define them.
## Property Types
```typescript
import { model } from "@medusajs/framework/utils"
const MyModel = model.define("my_model", {
// Primary key (required)
id: model.id().primaryKey(),
// Text
name: model.text(),
description: model.text().nullable(),
// Numbers
quantity: model.number(),
price: model.bigNumber(), // For high precision
// Boolean
is_active: model.boolean().default(true),
// Enum
status: model.enum(["draft", "published", "archived"]).default("draft"),
// Date/Time
published_at: model.dateTime().nullable(),
// JSON (for flexible data)
metadata: model.json().nullable(),
// Array
tags: model.array().nullable(),
})
```
## Property Modifiers
```typescript
model.text() // Required by default
model.text().nullable() // Allow null values
model.text().default("value") // Set default value
model.text().unique() // Unique constraint
model.text().primaryKey() // Set as primary key
```
## Relationships Within a Module
Define relationships between data models in the same module:
```typescript
// src/modules/blog/models/post.ts
import { model } from "@medusajs/framework/utils"
import { Comment } from "./comment"
export const Post = model.define("post", {
id: model.id().primaryKey(),
title: model.text(),
comments: model.hasMany(() => Comment, {
mappedBy: "post",
}),
})
// src/modules/blog/models/comment.ts
import { model } from "@medusajs/framework/utils"
import { Post } from "./post"
export const Comment = model.define("comment", {
id: model.id().primaryKey(),
content: model.text(),
post: model.belongsTo(() => Post, {
mappedBy: "comments",
}),
})
```
## Relationship Types
- `model.hasMany()` - One-to-many (post has many comments)
- `model.belongsTo()` - Many-to-one (comment belongs to post)
- `model.hasOne()` - One-to-one
- `model.manyToMany()` - Many-to-many
## Automatic Properties
Data models automatically include:
- `created_at` - Creation timestamp
- `updated_at` - Last update timestamp
- `deleted_at` - Soft delete timestamp
**Important**: Never add these properties explicitly to your model definitions.
## Generate and Run Migrations After Changes
After making changes to a data model, such as adding a property, you MUST generate migrations BEFORE running migrations:
```bash
npx medusa db:generate blog
npx medusa db:migrate
```

View File

@@ -0,0 +1,254 @@
# Error Handling in Medusa
Medusa provides the `MedusaError` class for consistent error responses across your API routes and custom code.
## Contents
- [Using MedusaError](#using-medusaerror)
- [Error Types](#error-types)
- [Error Response Format](#error-response-format)
- [Best Practices](#best-practices)
## Using MedusaError
Use `MedusaError` in API routes, workflows, and custom modules to throw errors that Medusa will automatically format and return to clients:
```typescript
import { MedusaError } from "@medusajs/framework/utils"
// Throw an error
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
"Product not found"
)
```
## Error Types
### NOT_FOUND
Use when a requested resource doesn't exist:
```typescript
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
"Product with ID 'prod_123' not found"
)
```
**HTTP Status**: 404
### INVALID_DATA
Use when request data fails validation or is malformed:
```typescript
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Email address is invalid"
)
```
**HTTP Status**: 400
### UNAUTHORIZED
Use when authentication is required but not provided:
```typescript
throw new MedusaError(
MedusaError.Types.UNAUTHORIZED,
"Authentication required to access this resource"
)
```
**HTTP Status**: 401
### NOT_ALLOWED
Use when the user is authenticated but doesn't have permission:
```typescript
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You don't have permission to delete this product"
)
```
**HTTP Status**: 403
### CONFLICT
Use when the operation conflicts with existing data:
```typescript
throw new MedusaError(
MedusaError.Types.CONFLICT,
"A product with this handle already exists"
)
```
**HTTP Status**: 409
### DUPLICATE_ERROR
Use when trying to create a duplicate resource:
```typescript
throw new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
"Email address is already registered"
)
```
**HTTP Status**: 422
### INVALID_STATE
Use when the resource is in an invalid state for the operation:
```typescript
throw new MedusaError(
MedusaError.Types.INVALID_STATE,
"Cannot cancel an order that has already been fulfilled"
)
```
**HTTP Status**: 400
## Error Response Format
Medusa automatically formats errors into a consistent JSON response:
```json
{
"type": "not_found",
"message": "Product with ID 'prod_123' not found"
}
```
## Best Practices
### 1. Use Specific Error Types
Choose the most appropriate error type for the situation:
```typescript
// ✅ GOOD: Uses specific error types
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const { id } = req.params
const query = req.scope.resolve("query")
const { data } = await query.graph({
entity: "product",
fields: ["id", "title"],
filters: { id },
})
if (!data || data.length === 0) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with ID '${id}' not found`
)
}
return res.json({ product: data[0] })
}
// ❌ BAD: Uses generic error
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const { id } = req.params
const query = req.scope.resolve("query")
const { data } = await query.graph({
entity: "product",
fields: ["id", "title"],
filters: { id },
})
if (!data || data.length === 0) {
throw new Error("Product not found") // Generic error
}
return res.json({ product: data[0] })
}
```
### 2. Provide Clear Error Messages
Error messages should be descriptive and help users understand what went wrong:
```typescript
// ✅ GOOD: Clear, specific message
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot create product: title must be at least 3 characters long"
)
// ❌ BAD: Vague message
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Invalid input"
)
```
### 3. Include Context in Error Messages
```typescript
// ✅ GOOD: Includes relevant context
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with ID '${productId}' not found`
)
// ✅ GOOD: Includes field name
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Invalid email format: '${email}'`
)
```
### 4. Handle Workflow Errors
When calling workflows from API routes, catch and transform errors:
```typescript
// ✅ GOOD: Catches and transforms workflow errors
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const { data } = req.validatedBody
try {
const { result } = await myWorkflow(req.scope).run({
input: { data },
})
return res.json({ result })
} catch (error) {
// Transform workflow errors into API errors
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Failed to create resource: ${error.message}`
)
}
}
```
### 5. Use Validation Middleware
Let validation middleware handle input validation errors:
```typescript
// ✅ GOOD: Middleware handles validation
// middlewares.ts
const MySchema = z.object({
email: z.string().email("Invalid email address"),
age: z.number().min(18, "Must be at least 18 years old"),
})
export const myMiddlewares: MiddlewareRoute[] = [
{
matcher: "/store/my-route",
method: "POST",
middlewares: [validateAndTransformBody(MySchema)],
},
]
// route.ts - No need to validate again
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const { email, age } = req.validatedBody // Already validated
// Your logic here
}
```

View File

@@ -0,0 +1,229 @@
# Frontend SDK Integration
## Contents
- [Frontend SDK Pattern](#frontend-sdk-pattern)
- [Locating the SDK](#locating-the-sdk)
- [Using sdk.client.fetch()](#using-sdkclientfetch)
- [React Query Pattern](#react-query-pattern)
- [Query Key Best Practices](#query-key-best-practices)
- [Error Handling](#error-handling)
- [Optimistic Updates](#optimistic-updates)
This guide covers how to integrate Medusa custom API routes with frontend applications using the Medusa SDK and React Query.
**Note:** API routes are also referred to as "endpoints" - these terms are interchangeable.
## Frontend SDK Pattern
### Locating the SDK
**IMPORTANT:** Never hardcode SDK import paths. Always locate where the SDK is instantiated in the project first.
Look for `@medusajs/js-sdk`
The SDK instance is typically exported as `sdk`:
```typescript
import { sdk } from "[LOCATE IN PROJECT]"
```
### Using sdk.client.fetch()
**⚠️ CRITICAL: ALWAYS use the Medusa JS SDK for ALL API requests - NEVER use regular fetch()**
**Why this is critical:**
- **Store API routes** require the publishable API key in headers
- **Admin API routes** require authentication headers
- **Regular fetch()** without these headers will cause errors
- The SDK automatically handles all required headers for you
**When to use what:**
- **Existing endpoints** (built-in Medusa routes): Use existing SDK methods like `sdk.store.product.list()`, `sdk.admin.order.retrieve()`
- **Custom endpoints** (your custom API routes): Use `sdk.client.fetch()` for custom routes
**⚠️ CRITICAL: The SDK handles JSON serialization automatically. NEVER use JSON.stringify() on the body.**
Call custom API routes using the SDK:
```typescript
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
// ✅ CORRECT - Pass object directly
const result = await sdk.client.fetch("/store/my-route", {
method: "POST",
body: {
email: "user@example.com",
name: "John Doe",
},
})
// ❌ WRONG - Don't use JSON.stringify
const result = await sdk.client.fetch("/store/my-route", {
method: "POST",
body: JSON.stringify({ // ❌ DON'T DO THIS!
email: "user@example.com",
}),
})
```
**Key points:**
- **The SDK handles JSON serialization automatically** - just pass plain objects
- **NEVER use JSON.stringify()** - this will break the request
- No need to set Content-Type headers - SDK adds them
- Session/JWT authentication is handled automatically
- Publishable API key is automatically added
### Built-in Endpoints vs Custom Endpoints
**⚠️ CRITICAL: Use the appropriate SDK method based on endpoint type**
```typescript
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
// ✅ CORRECT - Built-in endpoint: Use existing SDK method
const products = await sdk.store.product.list({
limit: 10,
offset: 0
})
// ✅ CORRECT - Custom endpoint: Use sdk.client.fetch()
const reviews = await sdk.client.fetch("/store/products/prod_123/reviews")
// ❌ WRONG - Using regular fetch for ANY endpoint
const products = await fetch("http://localhost:9000/store/products")
// ❌ Error: Missing publishable API key header!
// ❌ WRONG - Using regular fetch for custom endpoint
const reviews = await fetch("http://localhost:9000/store/products/prod_123/reviews")
// ❌ Error: Missing publishable API key header!
// ❌ WRONG - Using sdk.client.fetch() for built-in endpoint when SDK method exists
const products = await sdk.client.fetch("/store/products")
// ❌ Less type-safe than using sdk.store.product.list()
```
**Why this matters:**
- **Store routes** require `x-publishable-api-key` header - SDK adds it automatically
- **Admin routes** require `Authorization` and session cookie headers - SDK adds them automatically
- **Regular fetch()** doesn't include these headers → API returns authentication/authorization errors
- Using existing SDK methods provides **better type safety** and autocomplete
## React Query Pattern
Use `useQuery` for GET requests and `useMutation` for POST/DELETE:
```typescript
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
function MyComponent({ userId }: { userId: string }) {
const queryClient = useQueryClient()
// GET request - fetching data
const { data, isLoading } = useQuery({
queryKey: ["my-data", userId],
queryFn: () => sdk.client.fetch(`/store/my-route?userId=${userId}`),
enabled: !!userId,
})
// POST request - mutation with cache invalidation
const mutation = useMutation({
mutationFn: (input: { email: string }) =>
sdk.client.fetch("/store/my-route", { method: "POST", body: input }),
onSuccess: () => {
// Invalidate and refetch related queries
queryClient.invalidateQueries({ queryKey: ["my-data"] })
},
})
if (isLoading) return <p>Loading...</p>
return (
<div>
<p>{data?.title}</p>
<button
onClick={() => mutation.mutate({ email: "test@example.com" })}
disabled={mutation.isPending}
>
{mutation.isPending ? "Loading..." : "Submit"}
</button>
{mutation.isError && <p>Error occurred</p>}
</div>
)
}
```
**Key states:** `isLoading`, `isPending`, `isSuccess`, `isError`, `error`
## Query Key Best Practices
Structure query keys for effective cache management:
```typescript
// Good: Hierarchical structure
queryKey: ["products", productId]
queryKey: ["products", "list", { page, filters }]
// Invalidate all product queries
queryClient.invalidateQueries({ queryKey: ["products"] })
// Invalidate specific product
queryClient.invalidateQueries({ queryKey: ["products", productId] })
```
## Error Handling
Handle API errors gracefully:
```typescript
const mutation = useMutation({
mutationFn: (input) => sdk.client.fetch("/store/my-route", {
method: "POST",
body: input
}),
onError: (error) => {
console.error("Mutation failed:", error)
// Show error message to user
},
})
// In component
{mutation.isError && (
<p className="error">
{mutation.error?.message || "An error occurred"}
</p>
)}
```
## Optimistic Updates
Update UI immediately before server confirms:
```typescript
const mutation = useMutation({
mutationFn: (newItem) =>
sdk.client.fetch("/store/items", { method: "POST", body: newItem }),
onMutate: async (newItem) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ["items"] })
// Snapshot previous value
const previousItems = queryClient.getQueryData(["items"])
// Optimistically update
queryClient.setQueryData(["items"], (old) => [...old, newItem])
// Return context with snapshot
return { previousItems }
},
onError: (err, newItem, context) => {
// Rollback on error
queryClient.setQueryData(["items"], context.previousItems)
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ["items"] })
},
})
```

View File

@@ -0,0 +1,384 @@
# Module Links
## Contents
- [When to Use Links](#when-to-use-links)
- [Implementing Module Links - Workflow Checklist](#implementing-module-links---workflow-checklist)
- [Step 1: Defining a Link](#step-1-defining-a-link)
- [Step 2: Link Configuration Options](#step-2-link-configuration-options)
- [List Links (One-to-Many)](#list-links-one-to-many)
- [Delete Cascades](#delete-cascades)
- [Step 3: Sync Links (Run Migrations)](#step-3-sync-links-run-migrations)
- [Step 4: Managing Links](#step-4-managing-links)
- [Step 5: Querying Linked Data](#step-5-querying-linked-data)
- [Advanced: Link with Custom Columns](#advanced-link-with-custom-columns)
Module links create associations between data models in different modules while maintaining module isolation. Use links to connect your custom models to Commerce Module models (products, customers, orders, etc.).
## When to Use Links
- **Extend commerce entities**: Add brands to products, wishlists to customers
- **Cross-module associations**: Connect custom modules to each other
- **Maintain isolation**: Keep modules independent and reusable
## Implementing Module Links - Workflow Checklist
**IMPORTANT FOR CLAUDE CODE**: When implementing module links, use the TodoWrite tool to track your progress through these steps. This ensures you don't miss any critical steps and provides visibility to the user.
Create these tasks in your todo list:
- Optional: Add linked ID in custom data model (if one-to-one or one-to-many)
- Define the link in src/links/
- Configure list or delete cascade options if needed
- **CRITICAL: Run migrations: npx medusa db:migrate** (Never skip this step!)
- Create links in code using link.create() or createRemoteLinkStep
- Query linked data using query.graph()
- **CRITICAL: Run build to validate implementation** (catches type errors and issues)
## Optional: Add Linked ID in Custom Data Model
Add the ID of a linked data model in the custom data model if the custom data model belongs to it or extends it. Otherwise, skip this step.
For example, add ID of customer and product to custom product review model:
```typescript
import { model } from "@medusajs/framework/utils"
const Review = model.define("review", {
// other properties...
// ID of linked customer
customer_id: model.text(),
// ID of linked product
product_id: model.text()
})
export default Review
```
## Step 1: Defining a Link
**⚠️ CRITICAL RULE: Create ONE link definition per file.** Do NOT export an array of links from a single file.
Create link files in `src/links/`:
```typescript
// ✅ CORRECT - src/links/product-brand.ts (one link per file)
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import BrandModule from "../modules/brand"
export default defineLink(
ProductModule.linkable.product,
BrandModule.linkable.brand
)
```
**If one model links to multiple others, create multiple files:**
```typescript
// ✅ CORRECT - src/links/review-product.ts
export default defineLink(
ReviewModule.linkable.review,
ProductModule.linkable.product
)
// ✅ CORRECT - src/links/review-customer.ts
export default defineLink(
ReviewModule.linkable.review,
CustomerModule.linkable.customer
)
// ❌ WRONG - Don't export array of links from one file
export default [
defineLink(ReviewModule.linkable.review, ProductModule.linkable.product),
defineLink(ReviewModule.linkable.review, CustomerModule.linkable.customer),
] // This doesn't work!
```
**IMPORTANT:** The `.linkable` property is **automatically added** to all modules by Medusa. You do NOT need to add `.linkable()` or any linkable definition to your data models. Simply use `ModuleName.linkable.modelName` when defining links.
For example, if you have a `Review` data model in a `ReviewModule`:
- ✅ CORRECT: `ReviewModule.linkable.review` (works automatically)
- ❌ WRONG: Adding `.linkable()` method to the Review model definition (not needed, causes errors)
**⚠️ NEXT STEP**: After defining a link, you MUST immediately proceed to Step 3 to run migrations (`npx medusa db:migrate`). Do not skip this step!
## Step 2: Link Configuration Options
### List Links (One-to-Many)
Allow multiple records to link to one record:
```typescript
// A brand can have many products
export default defineLink(
{
linkable: ProductModule.linkable.product,
isList: true,
},
BrandModule.linkable.brand
)
```
### Delete Cascades
Automatically delete links when a record is deleted:
```typescript
export default defineLink(ProductModule.linkable.product, {
linkable: BrandModule.linkable.brand,
deleteCascade: true,
})
```
## Step 3: Sync Links (Run Migrations)
**⚠️ CRITICAL - DO NOT SKIP**: After defining links, you MUST run migrations to sync the link to the database. Without this step, the link will not work and you will get runtime errors.
```bash
npx medusa db:migrate
```
**Why this matters:**
- Links create database tables that store the relationships between modules
- Without migrations, these tables don't exist and link operations will fail
- This step is REQUIRED before creating any links in code or querying linked data
**Common mistake:** Defining a link in `src/links/` and immediately trying to use it in a workflow or query without running migrations first. Always run migrations immediately after defining a link.
## Step 4: Managing Links
**⚠️ CRITICAL - Link Order (Direction):** When creating or dismissing links, the order of modules MUST match the order in `defineLink()`. Mismatched order causes runtime errors.
```typescript
// Example link definition: product FIRST, then brand
export default defineLink(
ProductModule.linkable.product,
BrandModule.linkable.brand
)
```
### In Workflow Composition Functions
To create a link between records in workflow composition functions, use the `createRemoteLinkStep`:
```typescript
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import {
createWorkflow,
transform,
} from "@medusajs/framework/workflows-sdk"
const BRAND_MODULE = "brand"
export const myWorkflow = createWorkflow(
"my-workflow",
function (input) {
// ...
// ✅ CORRECT - Order matches defineLink (product first, then brand)
const linkData = transform({ input }, ({ input }) => {
return [
{
[Modules.PRODUCT]: {
product_id: input.product_id,
},
[BRAND_MODULE]: {
brand_id: input.brand_id,
},
},
]
})
createRemoteLinkStep(linkData)
// ...
}
)
// ❌ WRONG - Order doesn't match defineLink
const linkData = transform({ input }, ({ input }) => {
return [
{
[BRAND_MODULE]: {
brand_id: input.brand_id,
},
[Modules.PRODUCT]: {
product_id: input.product_id,
},
},
]
}) // Runtime error: link direction mismatch!
```
To dismiss (remove) a link between records in workflow composition functions, use the `dismissRemoteLinkStep`:
```typescript
import { Modules } from "@medusajs/framework/utils"
import { dismissRemoteLinkStep } from "@medusajs/medusa/core-flows"
import {
createWorkflow,
transform,
} from "@medusajs/framework/workflows-sdk"
const BRAND_MODULE = "brand"
export const myWorkflow = createWorkflow(
"my-workflow",
function (input) {
// ...
// Order MUST match defineLink (product first, then brand)
const linkData = transform({ input }, ({ input }) => {
return [
{
[Modules.PRODUCT]: {
product_id: input.product_id,
},
[BRAND_MODULE]: {
brand_id: input.brand_id,
},
},
]
})
dismissRemoteLinkStep(linkData)
// ...
}
)
```
### Outside Workflows
Outside workflows or in workflow steps, use the `link` utility to create and manage links between records. **Order MUST match `defineLink()` here too:**
```typescript
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
// In an API route or workflow step
const link = container.resolve(ContainerRegistrationKeys.LINK)
const BRAND_MODULE = "brand"
// ✅ CORRECT - Create a link (order matches defineLink: product first, then brand)
await link.create({
[Modules.PRODUCT]: { product_id: "prod_123" },
[BRAND_MODULE]: { brand_id: "brand_456" },
})
// ✅ CORRECT - Dismiss (remove) a link (same order: product first, then brand)
await link.dismiss({
[Modules.PRODUCT]: { product_id: "prod_123" },
[BRAND_MODULE]: { brand_id: "brand_456" },
})
// ❌ WRONG - Order doesn't match defineLink
await link.create({
[BRAND_MODULE]: { brand_id: "brand_456" },
[Modules.PRODUCT]: { product_id: "prod_123" },
}) // Runtime error: link direction mismatch!
```
## Step 5: Querying Linked Data
### Using query.graph() - Retrieve Linked Data
Use `query.graph()` to fetch data across linked modules. **Note**: `query.graph()` can retrieve linked data but **cannot filter by properties of linked modules** (data models in separate modules).
```typescript
const query = container.resolve("query")
// ✅ Get products with their linked brands (no cross-module filtering)
const { data: products } = await query.graph({
entity: "product",
fields: ["id", "title", "brand.*"], // brand.* fetches linked brand data
filters: {
id: "prod_123", // ✅ Filter by product properties only
},
})
// ✅ Get brands with their linked products
const { data: brands } = await query.graph({
entity: "brand",
fields: ["id", "name", "products.*"],
})
// ❌ DOES NOT WORK: Cannot filter products by linked brand properties
const { data: products } = await query.graph({
entity: "product",
fields: ["id", "title", "brand.*"],
filters: {
brand: {
name: "Nike" // ❌ Fails: brand is in a different module
}
}
})
```
### Using query.index() - Filter Across Linked Modules
To filter by properties of linked modules (separate modules with module links), use `query.index()` from the Index Module:
```typescript
const query = container.resolve("query")
// ✅ Filter products by linked brand name using Index Module
const { data: products } = await query.index({
entity: "product",
fields: ["*", "brand.*"],
filters: {
brand: {
name: "Nike" // ✅ Works with Index Module!
}
}
})
```
**Key Distinction:**
- **Same module relations** (e.g., Product → ProductVariant): Use `query.graph()` - filtering works ✅
- **Different module links** (e.g., Product → Brand): Use `query.index()` for filtering ✅
**Index Module Requirements:**
1. Install `@medusajs/index` package
2. Add to `medusa-config.ts`
3. Enable `MEDUSA_FF_INDEX_ENGINE=true` in `.env`
4. Run `npx medusa db:migrate`
5. Mark properties as `filterable` in link definition:
```typescript
// src/links/product-brand.ts
defineLink(
{ linkable: ProductModule.linkable.product, isList: true },
{ linkable: BrandModule.linkable.brand, filterable: ["id", "name"] }
)
```
See the [Querying Data reference](querying-data.md#querying-linked-data) for complete details on both methods.
## Advanced: Link with Custom Columns
Add extra data to the link table:
```typescript
export default defineLink(
ProductModule.linkable.product,
BrandModule.linkable.brand,
{
database: {
extraColumns: {
featured: {
type: "boolean",
defaultValue: "false",
},
},
},
}
)
```
Set custom column values when creating links:
```typescript
await link.create({
product: { product_id: "prod_123" },
brand: { brand_id: "brand_456" },
data: { featured: true },
})
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,417 @@
# Scheduled Jobs
Scheduled jobs are asynchronous functions that run automatically at specified intervals during the Medusa application's runtime. Use them for tasks like syncing products to third-party services, sending periodic reports, or cleaning up stale data.
## Contents
- [When to Use Scheduled Jobs](#when-to-use-scheduled-jobs)
- [Creating a Scheduled Job](#creating-a-scheduled-job)
- [Configuration Options](#configuration-options)
- [Executing Workflows in Scheduled Jobs](#executing-workflows-in-scheduled-jobs)
- [Cron Expression Examples](#cron-expression-examples)
- [Best Practices](#best-practices)
## When to Use Scheduled Jobs
Use scheduled jobs when you need to perform actions **periodically**:
- ✅ Syncing data with third-party services on a schedule
- ✅ Sending periodic reports (daily, weekly)
- ✅ Cleaning up stale data (expired carts, old sessions)
- ✅ Generating batch exports
- ✅ Recalculating aggregated data
**Don't use scheduled jobs for:**
- ❌ Reacting to events (use [subscribers](subscribers-and-events.md) instead)
- ❌ One-time tasks (use workflows directly)
- ❌ Real-time processing (use API routes + workflows)
**Scheduled Jobs vs Subscribers:**
- **Scheduled Job**: Finds carts updated >24h ago and sends emails (polling pattern)
- **Subscriber**: Reacts to `order.created` and sends an email (event-driven)
For most use cases, subscribers are preferred when you need to react to specific events.
## Creating a Scheduled Job
Create a TypeScript file in the `src/jobs/` directory:
```typescript
// src/jobs/sync-products.ts
import { MedusaContainer } from "@medusajs/framework/types"
export default async function syncProductsJob(container: MedusaContainer) {
const logger = container.resolve("logger")
logger.info("Starting product sync...")
// Resolve services from container
const productService = container.resolve("product")
const myService = container.resolve("my-custom-service")
try {
// Your job logic here
const products = await productService.listProducts({ active: true })
for (const product of products) {
// Process each product
await myService.syncToExternalSystem(product)
}
logger.info("Product sync completed successfully")
} catch (error) {
logger.error(`Product sync failed: ${error.message}`)
// Don't throw - let the job complete and retry on next schedule
}
}
export const config = {
name: "sync-products-daily", // Unique name for the job
schedule: "0 0 * * *", // Cron expression: midnight daily
}
```
## Configuration Options
```typescript
export const config = {
name: "my-job", // Required: unique identifier
schedule: "* * * * *", // Required: cron expression
numberOfExecutions: 3, // Optional: limit total scheduled executions
}
```
### Configuration Properties
- **name** (required): Unique identifier for the job across your application
- **schedule** (required): Cron expression defining when to run
- **numberOfExecutions** (optional): Maximum number of times to execute the job **according to its schedule**
**⚠️ CRITICAL - Understanding numberOfExecutions:**
`numberOfExecutions` limits how many times the job runs **on its schedule**, NOT immediately on server start.
```typescript
// ❌ WRONG UNDERSTANDING: This will NOT run immediately on server start
export const config = {
name: "test-job",
schedule: "0 0 * * *", // Daily at midnight
numberOfExecutions: 1, // Will run ONCE at the next midnight, not now!
}
// ✅ CORRECT: To test a job immediately, use a frequent schedule
export const config = {
name: "test-job",
schedule: "* * * * *", // Every minute
numberOfExecutions: 1, // Will run once at the next minute
}
// ✅ CORRECT: Testing with multiple runs
export const config = {
name: "test-job",
schedule: "*/5 * * * *", // Every 5 minutes
numberOfExecutions: 3, // Will run 3 times (at 0, 5, 10 minutes), then stop
}
```
**Key points:**
- The job waits for the first scheduled time before executing
- `numberOfExecutions: 1` with a daily schedule means it runs once the next day
- To test immediately, use a frequent schedule like `"* * * * *"` (every minute)
- After reaching `numberOfExecutions`, the job stops running permanently
## Executing Workflows in Scheduled Jobs
**⚠️ BEST PRACTICE**: Use workflows for mutations in scheduled jobs. This ensures proper error handling and rollback capabilities.
```typescript
// src/jobs/send-weekly-newsletter.ts
import { MedusaContainer } from "@medusajs/framework/types"
import { sendNewsletterWorkflow } from "../workflows/send-newsletter"
export default async function sendNewsletterJob(container: MedusaContainer) {
const logger = container.resolve("logger")
const query = container.resolve("query")
logger.info("Sending weekly newsletter...")
try {
// Query for data
const { data: customers } = await query.graph({
entity: "customer",
fields: ["id", "email"],
filters: {
newsletter_subscribed: true,
},
})
logger.info(`Found ${customers.length} subscribers`)
// Execute workflow
await sendNewsletterWorkflow(container).run({
input: {
customer_ids: customers.map((c) => c.id),
},
})
logger.info("Newsletter sent successfully")
} catch (error) {
logger.error(`Newsletter job failed: ${error.message}`)
}
}
export const config = {
name: "send-weekly-newsletter",
schedule: "0 0 * * 0", // Every Sunday at midnight
}
```
## Cron Expression Examples
Cron format: `minute hour day-of-month month day-of-week`
```typescript
// Every minute
schedule: "* * * * *"
// Every 5 minutes
schedule: "*/5 * * * *"
// Every hour at minute 0
schedule: "0 * * * *"
// Every day at midnight (00:00)
schedule: "0 0 * * *"
// Every day at 2:30 AM
schedule: "30 2 * * *"
// Every Sunday at midnight
schedule: "0 0 * * 0"
// Every Monday at 9 AM
schedule: "0 9 * * 1"
// First day of every month at midnight
schedule: "0 0 1 * *"
// Every weekday (Mon-Fri) at 6 PM
schedule: "0 18 * * 1-5"
// Every 6 hours
schedule: "0 */6 * * *"
```
**Tip**: Use [crontab.guru](https://crontab.guru) to build and validate cron expressions.
## Best Practices
### 1. Always Use Logging
```typescript
export default async function myJob(container: MedusaContainer) {
const logger = container.resolve("logger")
logger.info("Job started")
try {
// Job logic
logger.info("Job completed successfully")
} catch (error) {
logger.error(`Job failed: ${error.message}`, { error })
}
}
```
### 2. Handle Errors Gracefully
Don't throw errors at the top level - log them and let the job complete:
```typescript
// ❌ BAD: Throws and stops execution
export default async function myJob(container: MedusaContainer) {
const service = container.resolve("my-service")
const items = await service.getItems() // Might throw
// Job stops if this throws
}
// ✅ GOOD: Catches errors and logs
export default async function myJob(container: MedusaContainer) {
const logger = container.resolve("logger")
try {
const service = container.resolve("my-service")
const items = await service.getItems()
// Process items
} catch (error) {
logger.error(`Job failed: ${error.message}`)
// Job completes, will retry on next schedule
}
}
```
### 3. Make Jobs Idempotent
Design jobs to be safely re-runnable:
```typescript
// ✅ GOOD: Idempotent job
export default async function syncProducts(container: MedusaContainer) {
const logger = container.resolve("logger")
const myService = container.resolve("my-service")
// Check what's already synced
const lastSyncTime = await myService.getLastSyncTime()
// Only sync products updated since last sync
const { data: products } = await query.graph({
entity: "product",
filters: {
updated_at: { $gte: lastSyncTime },
},
})
// Sync products (upsert, don't insert)
for (const product of products) {
await myService.upsertToExternalSystem(product)
}
// Update last sync time
await myService.setLastSyncTime(new Date())
}
```
### 4. Use Workflows for Mutations
```typescript
// ✅ GOOD: Uses workflow for mutations
import { deleteCartsWorkflow } from "../workflows/delete-carts"
export default async function cleanupExpiredCarts(container: MedusaContainer) {
const logger = container.resolve("logger")
const query = container.resolve("query")
// Find expired carts
const { data: carts } = await query.graph({
entity: "cart",
fields: ["id"],
filters: {
updated_at: {
$lte: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago
},
},
})
logger.info(`Found ${carts.length} expired carts`)
// Use workflow for deletion (import at top of file)
await deleteCartsWorkflow(container).run({
input: {
cart_ids: carts.map((c) => c.id),
},
})
logger.info("Expired carts cleaned up")
}
```
### 5. Add Metrics/Monitoring
```typescript
export default async function myJob(container: MedusaContainer) {
const logger = container.resolve("logger")
const startTime = Date.now()
try {
// Job logic
const processed = 100 // Track what you processed
const duration = Date.now() - startTime
logger.info(`Job completed: ${processed} items in ${duration}ms`)
} catch (error) {
logger.error(`Job failed after ${Date.now() - startTime}ms`)
}
}
```
### 6. Test with Limited Executions
When testing, use a frequent schedule with limited executions:
```typescript
// ✅ CORRECT: Frequent schedule for immediate testing
export const config = {
name: "test-job",
schedule: "* * * * *", // Every minute
numberOfExecutions: 3, // Run 3 times (next 3 minutes), then stop
}
// ❌ WRONG: This won't help with testing
export const config = {
name: "test-job",
schedule: "0 0 * * *", // Daily at midnight
numberOfExecutions: 1, // Will only run ONCE at next midnight, not useful for testing
}
```
**Remember**: `numberOfExecutions` doesn't make the job run immediately - it limits how many times it runs on its schedule.
## Complete Example: Abandoned Cart Email Job
```typescript
// src/jobs/send-abandoned-cart-emails.ts
import { MedusaContainer } from "@medusajs/framework/types"
import { sendAbandonedCartEmailWorkflow } from "../workflows/send-abandoned-cart-email"
export default async function abandonedCartEmailJob(
container: MedusaContainer
) {
const logger = container.resolve("logger")
const query = container.resolve("query")
logger.info("Starting abandoned cart email job...")
try {
// Find carts updated more than 24 hours ago that haven't completed
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
const { data: carts } = await query.graph({
entity: "cart",
fields: ["id", "email", "customer_id"],
filters: {
updated_at: {
$lte: twentyFourHoursAgo,
},
completed_at: null,
email: { $ne: null }, // Must have email
},
})
logger.info(`Found ${carts.length} abandoned carts`)
// Process in batches
for (const cart of carts) {
try {
await sendAbandonedCartEmailWorkflow(container).run({
input: {
cart_id: cart.id,
email: cart.email,
},
})
logger.info(`Sent email for cart ${cart.id}`)
} catch (error) {
logger.error(`Failed to send email for cart ${cart.id}: ${error.message}`)
// Continue with other carts
}
}
logger.info("Abandoned cart email job completed")
} catch (error) {
logger.error(`Abandoned cart job failed: ${error.message}`)
}
}
export const config = {
name: "send-abandoned-cart-emails",
schedule: "0 */6 * * *", // Every 6 hours
}
```

View File

@@ -0,0 +1,544 @@
# Subscribers and Events
Subscribers are asynchronous functions that execute when specific events are emitted. Use them to perform actions after commerce operations, like sending confirmation emails when an order is placed.
## Contents
- [When to Use Subscribers](#when-to-use-subscribers)
- [Creating a Subscriber](#creating-a-subscriber)
- [Common Commerce Events](#common-commerce-events)
- [Accessing Event Data](#accessing-event-data)
- [Triggering Custom Events](#triggering-custom-events)
- [Best Practices](#best-practices)
## When to Use Subscribers
Use subscribers when you need to **react to events** that happen in your application:
- ✅ Send confirmation emails when orders are placed
- ✅ Sync data to external systems when products are updated
- ✅ Trigger webhooks when entities change
- ✅ Update analytics when customers are created
- ✅ Perform non-blocking side effects
**Don't use subscribers for:**
- ❌ Periodic tasks (use [scheduled jobs](scheduled-jobs.md) instead)
- ❌ Operations that must block the main flow (use workflows instead)
- ❌ Scheduling future tasks (subscribers execute immediately)
**Subscribers vs Scheduled Jobs:**
- **Subscriber**: Reacts to `order.placed` event and sends confirmation email (event-driven)
- **Scheduled Job**: Finds abandoned carts every 6 hours and sends emails (polling pattern)
## Creating a Subscriber
Create a TypeScript file in the `src/subscribers/` directory:
```typescript
// src/subscribers/order-placed.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
export default async function orderPlacedHandler({
event: { eventName, data },
container,
}: SubscriberArgs<{ id: string }>) {
const logger = container.resolve("logger")
logger.info(`Order ${data.id} was placed`)
// Resolve services
const orderService = container.resolve("order")
const notificationService = container.resolve("notification")
// Retrieve full order data
const order = await orderService.retrieveOrder(data.id, {
relations: ["customer", "items"],
})
// Send confirmation email
await notificationService.createNotifications({
to: order.customer.email,
template: "order-confirmation",
channel: "email",
data: { order },
})
logger.info(`Confirmation email sent for order ${data.id}`)
}
export const config: SubscriberConfig = {
event: "order.placed", // Single event
}
```
### Listening to Multiple Events
```typescript
// src/subscribers/product-changes.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
export default async function productChangesHandler({
event: { eventName, data },
container,
}: SubscriberArgs<{ id: string }>) {
const logger = container.resolve("logger")
logger.info(`Product event: ${eventName} for product ${data.id}`)
// Handle different events
switch (eventName) {
case "product.created":
// Handle product creation
break
case "product.updated":
// Handle product update
break
case "product.deleted":
// Handle product deletion
break
}
}
export const config: SubscriberConfig = {
event: ["product.created", "product.updated", "product.deleted"],
}
```
## Common Commerce Events
**⚠️ IMPORTANT**: Event data typically contains only the ID of the affected entity. You must retrieve the full data if needed.
### Order Events
```typescript
"order.placed" // Order was placed
"order.updated" // Order was updated
"order.canceled" // Order was canceled
"order.completed" // Order was completed
"order.shipment_created" // Shipment was created for order
```
### Product Events
```typescript
"product.created" // Product was created
"product.updated" // Product was updated
"product.deleted" // Product was deleted
```
### Customer Events
```typescript
"customer.created" // Customer was created
"customer.updated" // Customer was updated
```
### Cart Events
```typescript
"cart.created" // Cart was created
"cart.updated" // Cart was updated
```
### Auth Events
```typescript
"auth.password_reset" // Password reset was requested
```
### Invite Events
```typescript
"invite.created" // Invite was created (for admin users)
```
**For a complete list of events**, ask MedusaDocs for the specific module's events.
## Accessing Event Data
### Event Data Structure
```typescript
interface SubscriberArgs<T> {
event: {
eventName: string // e.g., "order.placed"
data: T // Event payload (usually contains { id: string })
}
container: MedusaContainer // DI container
}
```
### Retrieving Full Entity Data
**⚠️ IMPORTANT**: The `data` object typically only contains the entity ID. Retrieve the full entity data using services or query:
```typescript
// src/subscribers/order-placed.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
export default async function orderPlacedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const logger = container.resolve("logger")
const query = container.resolve("query")
// data.id contains the order ID
logger.info(`Handling order.placed event for order: ${data.id}`)
// Retrieve full order data with relations
const { data: orders } = await query.graph({
entity: "order",
fields: [
"id",
"email",
"total",
"customer.*",
"items.*",
"items.product.*",
],
filters: {
id: data.id,
},
})
const order = orders[0]
// Now you have the full order data
logger.info(`Order total: ${order.total}`)
logger.info(`Customer email: ${order.customer.email}`)
}
export const config: SubscriberConfig = {
event: "order.placed",
}
```
### Using Module Services
```typescript
export default async function productUpdatedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const productService = container.resolve("product")
// Retrieve product using service
const product = await productService.retrieveProduct(data.id, {
select: ["id", "title", "status"],
relations: ["variants"],
})
// Process product
}
```
## Triggering Custom Events
Emit custom events from workflows using the `emitEventStep`:
```typescript
// src/workflows/create-review.ts
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "@medusajs/medusa/core-flows"
const createReviewWorkflow = createWorkflow(
"create-review",
function (input: { product_id: string; rating: number }) {
// Create review step
const review = createReviewStep(input)
// Emit custom event
emitEventStep({
eventName: "review.created",
data: {
id: review.id,
product_id: input.product_id,
rating: input.rating,
},
})
return new WorkflowResponse({ review })
}
)
export default createReviewWorkflow
```
Then create a subscriber for the custom event:
```typescript
// src/subscribers/review-created.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
export default async function reviewCreatedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string; product_id: string; rating: number }>) {
const logger = container.resolve("logger")
const query = container.resolve("query")
logger.info(`Review ${data.id} created for product ${data.product_id}`)
// If rating is low, notify support
if (data.rating <= 2) {
const notificationService = container.resolve("notification")
await notificationService.createNotifications({
to: "support@example.com",
template: "low-rating-alert",
channel: "email",
data: {
review_id: data.id,
product_id: data.product_id,
rating: data.rating,
},
})
}
}
export const config: SubscriberConfig = {
event: "review.created",
}
```
## Best Practices
### 1. Always Use Logging
```typescript
export default async function mySubscriber({
event: { eventName, data },
container,
}: SubscriberArgs<{ id: string }>) {
const logger = container.resolve("logger")
logger.info(`Handling ${eventName} for ${data.id}`)
try {
// Subscriber logic
logger.info(`Successfully handled ${eventName}`)
} catch (error) {
logger.error(`Failed to handle ${eventName}: ${error.message}`)
}
}
```
### 2. Handle Errors Gracefully
Subscribers run asynchronously and don't block the main flow. Log errors but don't throw:
```typescript
// ✅ GOOD: Catches errors and logs
export default async function mySubscriber({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const logger = container.resolve("logger")
try {
// Subscriber logic that might fail
await sendEmail(data.id)
} catch (error) {
logger.error(`Failed to send email: ${error.message}`)
// Don't throw - subscriber completes gracefully
}
}
```
### 3. Keep Subscribers Fast and Non-Blocking
Subscribers should perform quick operations. For long-running tasks, consider:
- Queuing the task for background processing
- Using scheduled jobs instead
- Breaking the work into smaller steps
```typescript
// ✅ GOOD: Quick operation
export default async function orderPlacedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const notificationService = container.resolve("notification")
// Quick: Queue email for sending
await notificationService.createNotifications({
to: "customer@example.com",
template: "order-confirmation",
channel: "email",
data: { order_id: data.id },
})
}
```
### 4. Use Workflows for Mutations
If your subscriber needs to perform mutations, use workflows:
```typescript
// ✅ GOOD: Uses workflow for mutations
import { syncProductWorkflow } from "../workflows/sync-product"
export default async function productCreatedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const logger = container.resolve("logger")
// Execute workflow to sync to external system
try {
await syncProductWorkflow(container).run({
input: { product_id: data.id },
})
logger.info(`Product ${data.id} synced successfully`)
} catch (error) {
logger.error(`Failed to sync product ${data.id}: ${error.message}`)
}
}
```
### 5. Avoid Infinite Event Loops
Be careful when subscribing to events that trigger more events:
```typescript
// ❌ BAD: Can cause infinite loop
export default async function productUpdatedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const productService = container.resolve("product")
// This triggers another product.updated event!
await productService.updateProducts({
id: data.id,
metadata: { last_updated: new Date() },
})
}
// ✅ GOOD: Add guard condition
export default async function productUpdatedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const logger = container.resolve("logger")
const query = container.resolve("query")
// Retrieve product to check if we should update
const { data: products } = await query.graph({
entity: "product",
fields: ["id", "metadata"],
filters: { id: data.id },
})
const product = products[0]
// Guard: Only update if not already processed
if (!product.metadata?.processed) {
const productService = container.resolve("product")
await productService.updateProducts({
id: data.id,
metadata: { processed: true },
})
}
}
```
### 6. Make Subscribers Idempotent
Subscribers might be called multiple times for the same event. Design them to handle this:
```typescript
export default async function orderPlacedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const logger = container.resolve("logger")
const myService = container.resolve("my-service")
// Check if we've already processed this order
const processed = await myService.isOrderProcessed(data.id)
if (processed) {
logger.info(`Order ${data.id} already processed, skipping`)
return
}
// Process order
await myService.processOrder(data.id)
// Mark as processed
await myService.markOrderAsProcessed(data.id)
}
```
## Complete Example: Order Confirmation Email
```typescript
// src/subscribers/order-placed.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
export default async function sendOrderConfirmationEmail({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const logger = container.resolve("logger")
logger.info(`Sending order confirmation for order: ${data.id}`)
try {
const query = container.resolve("query")
// Retrieve full order data
const { data: orders } = await query.graph({
entity: "order",
fields: [
"id",
"display_id",
"email",
"total",
"currency_code",
"customer.first_name",
"customer.last_name",
"items.*",
"items.product.title",
"shipping_address.*",
],
filters: {
id: data.id,
},
})
if (!orders || orders.length === 0) {
logger.error(`Order ${data.id} not found`)
return
}
const order = orders[0]
// Send confirmation email
const notificationService = container.resolve("notification")
await notificationService.createNotifications({
to: order.email,
template: "order-confirmation",
channel: "email",
data: {
order_id: order.display_id,
customer_name: `${order.customer.first_name} ${order.customer.last_name}`,
items: order.items,
total: order.total,
currency: order.currency_code,
shipping_address: order.shipping_address,
},
})
logger.info(`Order confirmation email sent to ${order.email}`)
} catch (error) {
logger.error(
`Failed to send order confirmation for ${data.id}: ${error.message}`
)
}
}
export const config: SubscriberConfig = {
event: "order.placed",
}
```

View File

@@ -0,0 +1,225 @@
# Troubleshooting Common Medusa Backend Issues
This guide covers common errors and their solutions when building with Medusa.
## Contents
- [Module Registration Errors](#module-registration-errors)
- [API Route Errors](#api-route-errors)
- [Authentication Errors](#authentication-errors)
- [General Debugging Tips](#general-debugging-tips)
## Module Registration Errors
### Error: Module "X" not registered
```
Error: Module "my-module" is not registered in the container
```
**Cause**: Module not added to `medusa-config.ts` or server not restarted.
**Solution**:
1. Add module to `medusa-config.ts`:
```typescript
module.exports = defineConfig({
modules: [
{ resolve: "./src/modules/my-module" }
],
})
```
2. Restart the Medusa server
### Error: Cannot find module './modules/X'
```
Error: Cannot find module './modules/my-module'
```
**Cause**: Module path is incorrect or module structure is incomplete.
**Solution**:
1. Verify module structure:
```
src/modules/my-module/
├── models/
│ └── my-model.ts
├── service.ts
└── index.ts
```
2. Ensure `index.ts` exports the module correctly
3. Check path in `medusa-config.ts` matches actual directory
## API Route Errors
### Error: validatedBody is undefined
```
TypeError: Cannot read property 'email' of undefined
```
**Cause**: Forgot to add validation middleware or accessing `req.validatedBody` instead of `req.body`.
**Solution**:
1. Add validation middleware:
```typescript
// middlewares.ts
export const myMiddlewares: MiddlewareRoute[] = [
{
matcher: "/store/my-route",
method: "POST",
middlewares: [validateAndTransformBody(MySchema)],
},
]
```
2. Access `req.validatedBody` not `req.body`
### Error: queryConfig is undefined
```
TypeError: Cannot spread undefined
```
**Cause**: Using `...req.queryConfig` without setting up query config middleware.
**Solution**:
Add `validateAndTransformQuery` middleware:
```typescript
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
export const GetMyItemsSchema = createFindParams()
export default defineMiddlewares({
routes: [
{
matcher: "/store/my-items",
method: "GET",
middlewares: [
validateAndTransformQuery(GetMyItemsSchema, {
defaults: ["id", "name"],
isList: true,
}),
],
},
],
})
```
### Error: MedusaError not being formatted
```
Error: [object Object]
```
**Cause**: Throwing regular `Error` instead of `MedusaError`.
**Solution**:
```typescript
// ❌ WRONG
throw new Error("Not found")
// ✅ CORRECT
import { MedusaError } from "@medusajs/framework/utils"
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Not found")
```
### Error: Middleware not applying
```
Error: Route is not being validated
```
**Cause**: Middleware matcher doesn't match route path or middleware not registered.
**Solution**:
1. Check matcher pattern matches your route:
```typescript
// For route: /store/my-route
matcher: "/store/my-route" // Exact match
// For multiple routes: /store/my-route, /store/my-route/123
matcher: "/store/my-route*" // Wildcard
```
2. Ensure middleware is exported and registered in `api/middlewares.ts`
## Authentication Errors
### Error: auth_context is undefined
```
TypeError: Cannot read property 'actor_id' of undefined
```
**Cause**: Route is not protected or user is not authenticated.
**Solution**:
1. Check if route is under protected prefix (`/admin/*` or `/store/customers/me/*`)
2. If custom prefix, add authentication middleware:
```typescript
export default defineMiddlewares({
routes: [
{
matcher: "/custom/admin*",
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
},
],
})
```
3. For optional auth, check if `auth_context` exists:
```typescript
const userId = req.auth_context?.actor_id
if (!userId) {
// Handle unauthenticated case
}
```
## General Debugging Tips
### Enable Debug Logging
```bash
# Set log level to debug
LOG_LEVEL=debug npx medusa develop
```
### Log Values In Workflows with Transform
```typescript
import {
createStep,
createWorkflow,
StepResponse,
WorkflowResponse,
transform,
} from "@medusajs/framework/workflows-sdk"
const step1 = createStep(
"step-1",
async () => {
const message = "Hello from step 1!"
return new StepResponse(
message
)
}
)
export const myWorkflow = createWorkflow(
"my-workflow",
() => {
const response = step1()
const transformedMessage = transform(
{ response },
(data) => {
const upperCase = data.response.toUpperCase()
console.log("Transformed Data:", upperCase)
return upperCase
}
)
return new WorkflowResponse({
response: transformedMessage,
})
}
)
```

View File

@@ -0,0 +1,63 @@
# Workflow Hooks (Advanced)
Workflow hooks let you inject custom logic into existing Medusa workflows without recreating them. Use them to extend core commerce flows.
**Note:** Hooks run in-band (synchronously within the workflow). If your task can run in the background, use a subscriber instead for better performance.
## Basic Hook Pattern
```typescript
// src/workflows/hooks/product-created.ts
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
createProductsWorkflow.hooks.productsCreated(
// Hook handler
async ({ products, additional_data }, { container }) => {
if (!additional_data?.brand_id) {
return new StepResponse([], [])
}
const link = container.resolve("link")
// Link products to brand
const linkData = products.map((product) => ({
product: { product_id: product.id },
brand: { brand_id: additional_data.brand_id },
}))
await link.create(linkData)
return new StepResponse(linkData, linkData)
},
// Compensation (runs if workflow fails after this point)
async (linkData, { container }) => {
const link = container.resolve("link")
await link.dismiss(linkData)
}
)
```
## Common Workflow Hooks
- `createProductsWorkflow.hooks.productsCreated` - After products are created
- `createOrderWorkflow.hooks.orderCreated` - After an order is created
- Ask MedusaDocs for specific workflow hooks and their input parameters
## When to Use Hooks vs Subscribers
**Use workflow hooks when:**
- The logic must complete before the workflow finishes
- You need rollback/compensation capabilities
- The operation is critical to the workflow's success
**Use subscribers when:**
- The logic can run asynchronously in the background
- You don't need to block the main workflow
- Better performance is needed (hooks are synchronous)
## Hook Best Practices
1. **Return StepResponse**: Always wrap your return value
2. **Implement compensation**: Provide rollback logic for the compensation function
3. **Handle missing data gracefully**: Check for optional data and return early if not present
4. **Keep hooks lightweight**: For heavy operations, consider using subscribers instead

View File

@@ -0,0 +1,516 @@
# Creating Workflows
Workflows are the standard way to perform mutations (create, update, delete) in modules in Medusa. If you have built a custom module and need to perform mutations on models in the module, you should create a workflow.
## Creating Workflows - Implementation Checklist
**IMPORTANT FOR CLAUDE CODE**: When implementing workflows, use the TodoWrite tool to track your progress through these steps. This ensures you don't miss any critical steps and provides visibility to the user.
Create these tasks in your todo list:
- Define the input type for your workflow
- Create step function (one mutation per step)
- Add compensation function to steps for rollback
- Create workflow composition function
- Follow workflow composition rules (no async, no arrow functions, etc.)
- Return WorkflowResponse with results
- Test idempotency (workflow can be retried safely)
- **CRITICAL: Run build to validate implementation** (catches type errors and issues)
## Basic Workflow Structure
**File Organization:**
- **Recommended**: Create workflow steps in `src/workflows/steps/[step-name].ts`
- Workflow composition functions go in `src/workflows/[workflow-name].ts`
- This keeps steps reusable and organized
```typescript
// src/workflows/steps/create-my-model.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
type Input = {
my_key: string
}
// Note: a step should only do one mutation this ensures rollback mechanisms work
// For workflows that retry build your steps to be idempotent
export const createMyModelStep = createStep(
"create-my-model",
async (input: Input, { container }) => {
const myModule = container.resolve("my")
const [newMy] = await myModule.createMyModels({
...input,
})
return new StepResponse(
newMy,
newMy.id // explicit compensation input - otherwise defaults to step's output
)
},
// Optional compensation function
async (id, { container }) => {
const myModule = container.resolve("my")
await myModule.deleteMyModels(id)
}
)
// src/workflows/create-my-model.ts
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createMyModelStep } from "./steps/create-my-model"
type Input = {
my_key: string
}
const createMyModel = createWorkflow(
"create-my-model",
// Note: See "Workflow Composition Rules" section below for important constraints
// The workflow function must be a regular synchronous function (not async/arrow)
// No direct variable manipulation, conditionals, or date creation - use transform/when instead
function (input: Input) {
const newMy = createMyModelStep(input)
return new WorkflowResponse({
newMy,
})
}
)
export default createMyModel
```
## Workflow Composition Rules
The workflow composition function runs at application load time and has important limitations:
### Function Declaration
- ✅ Use regular synchronous functions
- ❌ No `async` functions
- ❌ No arrow functions (use `function` keyword)
### Using Steps Multiple Times
**⚠️ CRITICAL**: When using the same step multiple times in a workflow, you MUST rename each invocation AFTER the first invocation using `.config()` to avoid conflicts.
```typescript
// ✅ CORRECT - Rename each step invocation with .config()
export const processCustomersWorkflow = createWorkflow(
"process-customers",
function (input) {
const customers = transform({ ids: input.customer_ids }, (input) => input.ids)
// First invocation - no need to rename
const customer1 = fetchCustomerStep(customers[0])
// Second invocation - different name
const customer2 = fetchCustomerStep(customers[1]).config({
name: "fetch-customer-2"
})
const result = transform({ customer1, customer2 }, (data) => ({
customers: [data.customer1, data.customer2]
}))
return new WorkflowResponse(result)
}
)
// ❌ WRONG - Calling the same step multiple times without renaming
export const processCustomersWorkflow = createWorkflow(
"process-customers",
function (input) {
const customers = transform({ ids: input.customer_ids }, (input) => input.ids)
// This will cause runtime errors - duplicate step names
const customer1 = fetchCustomerStep(customers[0])
const customer2 = fetchCustomerStep(customers[1]) // ❌ Conflict!
return new WorkflowResponse({ customers: [customer1, customer2] })
}
)
```
**Why this matters:**
- Medusa uses step names to track execution state
- Duplicate names cause conflicts in the workflow execution engine
- Each step invocation needs a unique identifier
- The workflow will fail at runtime if steps aren't renamed
### Variable Operations
- ❌ No direct variable manipulation or concatenation → Use `transform({ in }, ({ in }) => \`Transformed: ${in}\`)` instead
- Variables lack values until execution time - all operations must use `transform()`
### Date/Time Operations
- ❌ No `new Date()` (will be fixed to load time) → Wrap in `transform()` for execution-time evaluation
### Conditional Logic
- ❌ No `if`/`else` statements → Use `when(input, (input) => input.is_active).then(() => { /* steps */ })` instead
- ❌ No ternary operators (`? :`) → Use `transform()` instead
- ❌ No nullish coalescing (`??`) → Use `transform()` instead
- ❌ No logical OR (`||`) → Use `transform()` instead
- ❌ No optional chaining (`?.`) → Use `transform()` instead
- ❌ No double negation (`!!`) → Use `transform()` instead
### Object Operations
- ❌ No object spreading (`...`) for destructuring or spreading properties → Use `transform()` to create new objects with desired properties
```typescript
// ❌ WRONG - Object spreading in workflow
const myWorkflow = createWorkflow(
"process-data",
function (input: WorkflowInput) {
const updatedData = {
...input.data,
newField: "value"
} // Won't work - spread operator not allowed
step1(updatedData)
})
// ✅ CORRECT - Use transform to create new objects
import { transform } from "@medusajs/framework/workflows-sdk"
const myWorkflow = createWorkflow(
"process-data",
function (input: WorkflowInput) {
const updatedData = transform(
{ input },
(data) => ({
...data.input.data,
newField: "value"
})
)
step1(updatedData)
})
```
### Loops
- ❌ No `for`/`while` loops → Use alternatives below based on your use case
Workflow composition functions run at application load time to define the workflow structure, not to execute logic. Loops cannot be used directly in the composition function. Instead, use these patterns:
**Alternative 1: Loop in Calling Code (Repeat entire workflow)**
When you need to execute a workflow multiple times (e.g., once per item in an array), wrap the workflow execution in a loop in the code that calls the workflow:
```typescript
// ❌ WRONG - Loop inside workflow composition
const myWorkflow = createWorkflow(
"hello-world",
function (input: WorkflowInput) {
for (const item of input.items) {
step1(item) // Won't work - loop runs at load time, not execution time
}
})
// ✅ CORRECT - Loop in calling code
// API route that calls the workflow
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/my-workflow"
export async function POST(
req: MedusaRequest,
res: MedusaResponse
) {
const { items } = req.body
// Execute the workflow once for each item
for (const item of items) {
await myWorkflow(req.scope)
.run({ item })
}
res.status(200).send({ success: true })
}
// Workflow definition - processes a single item
const myWorkflow = createWorkflow(
"hello-world",
function (input: WorkflowInput) {
step1(input.item)
})
```
**Alternative 2: Use `transform` for Array Operations (Prepare step inputs)**
When you need to iterate over an array to prepare inputs for a step, use `transform()` to map over the array:
```typescript
// ❌ WRONG - Loop to build array
const myWorkflow = createWorkflow(
"hello-world",
function (input: WorkflowInput) {
const stepInputs = []
for (const item of input.items) {
stepInputs.push({ id: item.id }) // Won't work - loop runs at load time
}
step1(stepInputs)
})
// ✅ CORRECT - Use transform to map array
import { transform } from "@medusajs/framework/workflows-sdk"
const myWorkflow = createWorkflow(
"hello-world",
function (input: WorkflowInput) {
const stepInputs = transform(
{
input,
},
(data) => {
// This function runs at execution time
return data.input.items.map((item) => ({ id: item.id }))
}
)
step1(stepInputs)
})
```
**Why this matters:**
- The workflow composition function runs once at application load time to define the structure
- Loops would execute at load time with no data, not at execution time with actual input
- Alternative 1 repeats the entire workflow (including rollback capability) for each item
- Alternative 2 processes arrays within a single workflow execution using `transform()`
### Error Handling
- ❌ No `try-catch` blocks → See error handling patterns in Medusa documentation
### Return Values
- ✅ Only return serializable values (primitives, plain objects)
- ❌ No non-serializable types (Maps, Sets, etc.)
- For buffers: Return as object property, then recreate with `Buffer.from()` when processing results
## Step Best Practices
1. **One mutation per step**: Ensures rollback mechanisms work correctly
2. **Idempotency**: Design steps to be safely retryable
3. **Explicit compensation input**: Specify what data the compensation function needs if different from step output
4. **Return StepResponse**: Always wrap your return value in `StepResponse`
## Reusing Built-in Medusa Steps
**⚠️ IMPORTANT**: Before creating custom steps, check if Medusa provides a built-in step for your use case. Reusing built-in steps is preferred over creating custom ones.
### Common Built-in Steps to Reuse
**Creating Links Between Modules:**
**⚠️ CRITICAL - Link Order (Direction):** When creating links, the order of modules in `createRemoteLinkStep` MUST match the order in `defineLink()`. Mismatched order causes runtime errors.
```typescript
// Link definition in src/links/review-product.ts
import { defineLink } from "@medusajs/framework/utils"
import ReviewModule from "../modules/review"
import ProductModule from "@medusajs/medusa/product"
// Order: review FIRST, then product
export default defineLink(
{
linkable: ReviewModule.linkable.review,
isList: true,
},
ProductModule.linkable.product
)
```
```typescript
// ✅ CORRECT - Order matches defineLink (review first, then product)
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { REVIEW_MODULE } from "../modules/review"
export const createReviewWorkflow = createWorkflow(
"create-review",
function (input) {
const review = createReviewStep(input)
// Order MUST match defineLink: review first, then product
const linkData = transform({ review, input }, ({ review, input }) => [{
[REVIEW_MODULE]: {
review_id: review.id,
},
[Modules.PRODUCT]: {
product_id: input.product_id,
},
}])
createRemoteLinkStep(linkData)
return new WorkflowResponse({ review })
}
)
// ❌ WRONG - Order doesn't match defineLink (product first, then review)
const linkData = transform({ review, input }, ({ review, input }) => [{
[Modules.PRODUCT]: {
product_id: input.product_id,
},
[REVIEW_MODULE]: {
review_id: review.id,
},
}]) // Runtime error: link direction mismatch!
```
```typescript
// ❌ WRONG - Don't create custom link steps
const createReviewLinkStep = createStep(
"create-review-link",
async ({ reviewId, productId }, { container }) => {
const link = container.resolve("link")
await link.create({
product: { product_id: productId },
review: { review_id: reviewId },
})
// This duplicates functionality that createRemoteLinkStep provides
}
)
```
**Removing Links:**
```typescript
// ✅ CORRECT - Use Medusa's built-in dismissRemoteLinkStep
import { dismissRemoteLinkStep } from "@medusajs/medusa/core-flows"
export const deleteReviewWorkflow = createWorkflow(
"delete-review",
function (input) {
const linkData = transform({ input }, ({ input }) => [{
[Modules.PRODUCT]: { product_id: input.product_id },
review: { review_id: input.review_id },
}])
dismissRemoteLinkStep(linkData)
deleteReviewStep(input)
return new WorkflowResponse({ success: true })
}
)
```
**Querying Data in Workflows:**
```typescript
// ✅ CORRECT - Use Medusa's built-in useQueryGraphStep
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
export const getProductReviewsWorkflow = createWorkflow(
"get-product-reviews",
function (input) {
// Query product with reviews using built-in step
const { data: products } = useQueryGraphStep({
entity: "product",
fields: ["id", "title", "reviews.*"],
filters: {
id: input.product_id,
},
})
return new WorkflowResponse({ product: products[0] })
}
)
// ❌ WRONG - Don't create custom query steps
const queryProductStep = createStep(
"query-product",
async ({ productId }, { container }) => {
const query = container.resolve("query")
const { data } = await query.graph({
entity: "product",
fields: ["id", "title", "reviews.*"],
filters: { id: productId },
})
return new StepResponse(data[0])
}
)
// This duplicates functionality that useQueryGraphStep provides
```
**Why reuse built-in steps:**
- Already tested and optimized by Medusa
- Handles edge cases and error scenarios
- Maintains consistency with Medusa's internal workflows
- Includes proper compensation/rollback logic
- Less code to maintain
**Other common built-in steps to look for:**
- Event emission steps
- Notification steps
- Inventory management steps
- Payment processing steps
Check Medusa documentation or `@medusajs/medusa/core-flows` for available built-in steps before creating custom ones.
## Business Logic and Validation Placement
**CRITICAL**: All business logic and validation must be performed inside workflow steps, NOT in API routes.
### ✅ CORRECT - Validation in Workflow Step
```typescript
// src/workflows/steps/delete-review.ts
export const deleteReviewStep = createStep(
"delete-review",
async ({ reviewId, customerId }: Input, { container }) => {
const reviewModule = container.resolve("review")
// Validation happens inside the step
const review = await reviewModule.retrieveReview(reviewId)
if (review.customer_id !== customerId) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You can only delete your own reviews"
)
}
await reviewModule.deleteReviews(reviewId)
return new StepResponse({ id: reviewId }, reviewId)
},
async (reviewId, { container }) => {
// Compensation: restore the review if needed
}
)
```
### ❌ WRONG - Validation in API Route
```typescript
// src/api/store/reviews/[id]/route.ts
export async function DELETE(req: MedusaRequest, res: MedusaResponse) {
const { id } = req.params
const customerId = req.auth_context.actor_id
// ❌ WRONG: Don't validate business rules in the route
const reviewModule = req.scope.resolve("review")
const review = await reviewModule.retrieveReview(id)
if (review.customer_id !== customerId) {
throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Not your review")
}
// ❌ WRONG: Don't call workflows after manual validation
const { result } = await deleteReviewWorkflow(req.scope).run({
input: { reviewId: id }
})
}
```
**Why this matters:**
- Workflows are the single source of truth for business logic
- Validation in routes bypasses workflow rollback mechanisms
- Makes testing harder and logic harder to reuse
- Breaks the Module → Workflow → API Route architecture
## Advanced Features
Workflows have advanced options to define retries, async behavior, pausing for human confirmation, and much more. Ask MedusaDocs for more details if these are relevant to your use case.

View File

@@ -0,0 +1,23 @@
---
name: db-generate
description: Generate database migrations for a Medusa module
argument-hint: <module-name>
allowed-tools: Bash(npx medusa db:generate:*)
---
# Generate Database Migrations
Generate database migrations for the specified Medusa module.
The user will provide the module name as an argument (e.g., `brand`, `product`, `custom-module`).
For example: `/medusa-dev:db-generate brand`
Use the Bash tool to execute the command `npx medusa db:generate <module-name>`, replacing `<module-name>` with the provided argument.
Report the results to the user, including:
- The module name for which migrations were generated
- Migration file name or location
- Any errors or warnings
- Next steps (running `npx medusa db:migrate` to apply the migrations)

View File

@@ -0,0 +1,17 @@
---
name: db-migrate
description: Run database migrations in Medusa
allowed-tools: Bash(npx medusa db:migrate:*)
---
# Run Database Migrations
Execute the Medusa database migration command to apply pending migrations.
Use the Bash tool to execute: `npx medusa db:migrate`
Report the migration results to the user, including:
- Number of migrations applied
- Any errors that occurred
- Success confirmation

View File

@@ -0,0 +1,25 @@
---
name: new-user
description: Create an admin user in Medusa
argument-hint: <email> <password>
allowed-tools: Bash(npx medusa user:*)
---
# Create Admin User
Create a new admin user in Medusa with the specified email and password.
The user will provide two arguments:
- First argument: email address
- Second argument: password
For example: `/medusa-dev:user admin@test.com supersecret`
Use the Bash tool to execute the command `npx medusa user -e <email> -p <password>`, replacing `<email>` with the first argument and `<password>` with the second argument.
Report the results to the user, including:
- Confirmation that the admin user was created successfully
- The email address of the created user
- Any errors that occurred
- Next steps (e.g., logging in to the admin dashboard)

View File

@@ -0,0 +1,421 @@
---
name: storefront-best-practices
description: ALWAYS use this skill when working on ecommerce storefronts, online stores, shopping sites. Use for ANY storefront component including checkout pages, cart, payment flows, product pages, product listings, navigation, homepage, or ANY page/component in a storefront. CRITICAL for adding checkout, implementing cart, integrating Medusa backend, or building any ecommerce functionality. Framework-agnostic (Next.js, SvelteKit, TanStack Start, React, Vue). Provides patterns, decision frameworks, backend integration guidance.
---
# Ecommerce Storefront Best Practices
Comprehensive guidance for building modern, high-converting ecommerce storefronts covering UI/UX patterns, component design, layout structures, SEO optimization, and mobile responsiveness.
## When to Apply
**ALWAYS load this skill when working on ANY storefront task:**
- **Adding checkout page/flow** - Payment, shipping, order placement
- **Implementing cart** - Cart page, cart popup, add to cart functionality
- **Building product pages** - Product details, product listings, product grids
- **Creating navigation** - Navbar, megamenu, footer, mobile menu
- **Integrating Medusa backend** - SDK setup, cart, products, payment
- **Any storefront component** - Homepage, search, filters, account pages
- Building new ecommerce storefronts from scratch
- Improving existing shopping experiences and conversion rates
- Optimizing for usability, accessibility, and SEO
- Designing mobile-responsive ecommerce experiences
**Example prompts that should trigger this skill:**
- "Add a checkout page"
- "Implement shopping cart"
- "Create product listing page"
- "Connect to Medusa backend"
- "Add navigation menu"
- "Build homepage for store"
## CRITICAL: Load Reference Files When Needed
**⚠️ ALWAYS load `reference/design.md` BEFORE creating ANY UI component**
- Discovers existing design tokens (colors, fonts, spacing, patterns)
- Prevents introducing inconsistent styles
- Provides guardrails for maintaining brand consistency
- **Required for every component, not just new storefronts**
**Load these references based on what you're implementing:**
- **Starting a new storefront?** → MUST load `reference/design.md` first to discover user preferences
- **Connecting to backend API?** → MUST load `reference/connecting-to-backend.md` first
- **Connecting to Medusa backend?** → MUST load `reference/medusa.md` for SDK setup, pricing, regions, and Medusa patterns
- **Implementing homepage?** → MUST load `reference/components/navbar.md`, `reference/components/hero.md`, `reference/components/footer.md`, and `reference/layouts/home-page.md`
- **Implementing navigation?** → MUST load `reference/components/navbar.md` and optionally `reference/components/megamenu.md`
- **Building product listing?** → MUST load `reference/layouts/product-listing.md` first
- **Building product details?** → MUST load `reference/layouts/product-details.md` first
- **Implementing checkout?** → MUST load `reference/layouts/checkout.md` first
- **Optimizing for SEO?** → MUST load `reference/seo.md` first
- **Optimizing for mobile?** → MUST load `reference/mobile-responsiveness.md` first
**Minimum requirement:** Load at least 1-2 reference files relevant to your specific task before implementing.
## Planning and Implementation Workflow
**IMPORTANT: If you create a plan for implementing storefront features, include the following in your plan:**
When implementing each component, page, layout, or feature in the plan:
1. **Refer back to this skill** before starting implementation
2. **Load relevant reference files** listed above for the specific component/page you're building
3. **Follow the patterns and guidance** in the reference files
4. **Check common mistakes** sections to avoid known pitfalls
**Example plan structure:**
```
Task 1: Implement Navigation
- Load reference/components/navbar.md
- Follow patterns from navbar.md (dynamic category fetching, cart visibility, etc.)
- Refer to skill for common mistakes (e.g., hardcoding categories)
Task 2: Implement Product Listing Page
- Load reference/layouts/product-listing.md
- Follow pagination/filtering patterns from product-listing.md
- Use reference/components/product-card.md for product grid items
- Check skill for backend integration guidance
Task 3: Implement Checkout Flow
- Load reference/layouts/checkout.md
- Load reference/medusa.md for Medusa payment integration
- Follow component architecture recommendations (separate step components)
- Refer to skill for payment method fetching requirements
```
**Why this matters:**
- Plans provide high-level strategy
- Reference files provide detailed implementation patterns
- Skill file contains critical mistakes to avoid
- Following this workflow ensures consistency and best practices
## Critical Ecommerce-Specific Patterns
### Accessibility
- **CRITICAL: Cart count updates require `aria-live="polite"`** - Screen readers won't announce without it
- Ensure keyboard navigation for all cart/checkout interactions
### Mobile
- **Sticky bottom elements MUST use `env(safe-area-inset-bottom)`** - iOS home indicator will cut off purchase buttons otherwise
- 44px minimum touch targets for cart actions, variant selectors, quantity buttons
### Performance
- **ALWAYS add `loading="lazy"` to product images below fold** - Don't rely on browser defaults
- Optimize product images for mobile (<500KB) - Most ecommerce traffic is mobile
### Conversion Optimization
- Clear CTAs throughout shopping flow
- Minimal friction in checkout (guest checkout if supported)
- Trust signals (reviews, security badges, return policy) near purchase buttons
- Clear pricing and shipping information upfront
### SEO
- **Product schema (JSON-LD) required** - Critical for Google Shopping and rich snippets
- Use [PageSpeed Insights](https://pagespeed.web.dev/) to measure Core Web Vitals
### Visual Design
- **NEVER use emojis** in storefront UI - Use icons or images instead (unprofessional, accessibility issues)
### Backend Integration
- **Backend detection**: If in monorepo, check for backend directory. If unsure, ask user which backend is used.
- **NEVER hardcode dynamic content**: Always fetch categories, regions, products, shipping options, etc. from backend - they change frequently
- Never assume API structure - verify endpoints and data formats
### ⚠️ CRITICAL: Backend SDK Method Verification Workflow
**YOU MUST FOLLOW THIS EXACT WORKFLOW BEFORE WRITING CODE THAT CONNECTS TO BACKEND:**
**Step 1: PAUSE - Do NOT write code yet**
- You are about to write code that calls a backend API or SDK method (e.g., Medusa SDK, REST API, GraphQL)
- **STOP** - Do not proceed to code without verification
**Step 2: QUERY the documentation or MCP server**
- **If MCP server available**: Query it for the exact method (for example, medusa MCP)
- **If no MCP server**: Search official documentation
- **Find**: Exact method name, parameters, return type
**Step 3: VERIFY what you found**
- State out loud to the user: "I need to verify the correct method for [operation]. Let me check [MCP server/documentation]."
- Show the user what you found: "According to [source], the method is `sdk.store.cart.methodName(params)`"
- Confirm the method signature and parameters
**Step 4: ONLY THEN write the code**
- Now you can write code using the verified method
- Use the exact signature you found
**Step 5: CHECK for TypeScript errors**
- After writing the code, check for any TypeScript/type errors related to the SDK
- If you see type errors on SDK methods, it means you used an incorrect method name or wrong parameters
- **Type errors are a sign you didn't verify correctly** - Go back to Step 2
**THIS IS NOT OPTIONAL - THIS IS MANDATORY ERROR PREVENTION**
**It is a CRITICAL ERROR to:**
- ❌ Write code that calls backend APIs/SDKs without explicitly querying docs/MCP first
- ❌ Guess method names or parameters
- ❌ Ignore TypeScript errors on SDK methods (errors indicate incorrect method usage)
- ❌ Copy examples from this skill without verification (examples may be outdated)
- ❌ Assume SDK methods match REST API endpoints
**For Medusa specifically:**
- **Medusa pricing**: Display prices as-is - DO NOT divide by 100 (unlike Stripe, Medusa stores prices in display format)
- **Medusa MCP server**: https://docs.medusajs.com/mcp - Recommend setup if not installed
- Load `reference/medusa.md` for Medusa-specific patterns (regions, pricing, etc.)
### Routing Patterns
- **ALWAYS use dynamic routes** for products and categories - NEVER create static pages for individual items
- Product pages: Use dynamic routes like `/products/[handle]` or `/products/$handle`, NOT `/products/shirt.tsx`
- Category pages: Use dynamic routes like `/categories/[handle]` or `/categories/$handle`, NOT `/categories/women.tsx`
- Framework-specific patterns:
- **Next.js App Router**: `app/products/[handle]/page.tsx` or `app/products/[id]/page.tsx`
- **Next.js Pages Router**: `pages/products/[handle].tsx`
- **SvelteKit**: `routes/products/[handle]/+page.svelte`
- **TanStack Start**: `routes/products/$handle.tsx`
- **Remix**: `routes/products.$handle.tsx`
- Why: Dynamic routes scale to any number of products/categories without creating individual files
- Static routes are unmaintainable and don't scale (imagine creating 1000 product files)
## Pattern Selection Guides
When you need to choose between implementation patterns, load the relevant reference file:
- **Checkout strategy** (single-page vs multi-step) → Load `reference/layouts/checkout.md`
- **Navigation strategy** (dropdown vs megamenu) → Load `reference/components/navbar.md` and `reference/components/megamenu.md`
- **Product listing strategy** (pagination vs infinite scroll vs load more) → Load `reference/layouts/product-listing.md`
- **Search strategy** (autocomplete vs filters vs natural language) → Load `reference/components/search.md`
- **Mobile vs desktop priorities** → Load `reference/mobile-responsiveness.md`
- **Variant selection** (text vs swatches vs configurator) → Load `reference/layouts/product-details.md`
- **Cart pattern** (popup vs drawer vs page navigation) → Load `reference/components/cart-popup.md` and `reference/layouts/cart.md`
- **Trust signals strategy** → Load `reference/layouts/product-details.md` and `reference/layouts/checkout.md`
Each reference file contains decision frameworks with specific criteria to help you choose the right pattern for your context.
## Quick Reference
### General
```
reference/connecting-to-backend.md - Framework detection, API setup, backend integration patterns
reference/medusa.md - Medusa SDK integration, pricing, regions, TypeScript types
reference/design.md - User preferences, brand identity, design systems
reference/seo.md - Meta tags, structured data, Core Web Vitals
reference/mobile-responsiveness.md - Mobile-first design, responsive breakpoints, touch interactions
```
### Components
```
reference/components/navbar.md - Desktop/mobile navigation, logo, menu, cart icon, load for ALL pages
reference/components/megamenu.md - Category organization, featured products, mobile alternatives
reference/components/cart-popup.md - Add-to-cart feedback, mini cart display
reference/components/country-selector.md - Country/region selection, currency, pricing, Medusa regions
reference/components/breadcrumbs.md - Category hierarchy, structured data markup
reference/components/search.md - Search input, autocomplete, results, filters
reference/components/product-reviews.md - Review display, rating aggregation, submission
reference/components/hero.md - Hero layouts, CTA placement, image optimization
reference/components/popups.md - Newsletter signup, discount popups, exit-intent
reference/components/footer.md - Content organization, navigation, social media, load for ALL pages
reference/components/product-card.md - Product images, pricing, add to cart, badges
reference/components/product-slider.md - Carousel implementation, mobile swipe, accessibility
```
### Layouts
```
reference/layouts/home-page.md - Hero, featured categories, product listings
reference/layouts/product-listing.md - Grid/list views, filters, sorting, pagination
reference/layouts/product-details.md - Image gallery, variant selection, related products
reference/layouts/cart.md - Cart items, quantity updates, promo codes
reference/layouts/checkout.md - Multi-step/single-page, address forms, payment
reference/layouts/order-confirmation.md - Order number, summary, delivery info
reference/layouts/account.md - Dashboard, order history, address book
reference/layouts/static-pages.md - FAQ, about, contact, shipping/returns policies
```
### Features
```
reference/features/wishlist.md - Add to wishlist, wishlist page, move to cart
reference/features/promotions.md - Promotional banners, discount codes, sale badges
```
## Common Implementation Patterns
### Starting a New Storefront
**IMPORTANT: For each step below, load the referenced files BEFORE implementing that step.**
```
1. Discovery Phase → Read design.md for user preferences
2. Foundation Setup → Read connecting-to-backend.md (or medusa.md for Medusa), mobile-responsiveness.md, seo.md
3. Core Components → Implement navbar.md, footer.md
4. Home Page → Read home-page.md
5. Product Browsing → Read product-listing.md, product-card.md, search.md
6. Product Details → Read product-details.md, product-reviews.md
7. Cart & Checkout → Read cart-popup.md, cart.md, checkout.md, order-confirmation.md
8. User Account → Read account.md
9. Additional Features → Read wishlist.md, promotions.md
10. Optimization → SEO audit (seo.md), mobile testing (mobile-responsiveness.md)
```
Even if you create an implementation plan, refer back to the skill and load relevant reference files when implementing each step.
### Shopping Flow Pattern
```
Browse → View → Cart → Checkout
Browse: home-page.md → product-listing.md
View: product-details.md + product-reviews.md
Cart: cart-popup.md → cart.md
Checkout: checkout.md → order-confirmation.md
```
### Component Selection Guide
**For product grids and filtering**`product-listing.md` and `product-card.md`
**For product cards**`product-card.md`
**For navigation**`navbar.md` and `megamenu.md`
**For search functionality**`search.md`
**For checkout flow**`checkout.md`
**For promotions and sales**`promotions.md`
## Design Considerations
Before implementing, consider:
1. **User preferences** - Read `design.md` to discover design style preferences
2. **Brand identity** - Colors, typography, tone that match the brand
3. **Target audience** - B2C vs B2B, demographics, device usage
4. **Product type** - Fashion vs electronics vs groceries affect layout choices
5. **Business requirements** - Multi-currency, multi-language, region-specific
6. **Backend system** - API structure affects component implementation
## Integration with Medusa
[Medusa](https://medusajs.com) is a modern, flexible ecommerce backend. Consider Medusa when:
- Building a new ecommerce storefront
- Need a headless commerce solution
- Want built-in support for multi-region, multi-currency
- Need powerful promotion and discount engine
- Require flexible product modeling
For detailed Medusa integration guidance, see `reference/medusa.md`. For general backend patterns, see `reference/connecting-to-backend.md`.
### Framework Agnostic
All guidance is framework-agnostic. Examples use React/TypeScript where code demonstrations are helpful, but patterns apply to:
- Next.js
- SvelteKit
- Tanstack Start
- Any modern frontend framework
## Minimum Viable Features
**Mandatory for launch (core shopping flow):**
- Navbar with cart, categories, search
- Product listing with filtering and pagination
- Product details with variant selection
- Add to cart functionality
- Cart page with item management
- Checkout flow (shipping, payment, review)
- Order confirmation page
**Nice-to-have (add if time permits):**
- Related products recommendations
- Product reviews and ratings
- Wishlist functionality
- Image zoom on product pages
- Bottom navigation on mobile
- Mega-menu for navigation
- Newsletter signup
- Product comparison
- Quick view modals
**User-dependent (ask before implementing):**
- Guest checkout vs login-required
- Account dashboard features
- Multi-language support
- Multi-currency support
- Live chat support
## Top Ecommerce Mistakes to Avoid
Before implementing, watch out for these common ecommerce-specific pitfalls:
**1. Cart and Navigation Mistakes**
- ❌ Hiding cart indicator in mobile hamburger menu (keep always visible)
- ❌ Not showing real-time cart count updates
-**CRITICAL: Missing `aria-live="polite"` on cart count** - Screen readers won't announce cart updates without it
- ❌ Not displaying variant details (size, color, etc.) in cart popup - only showing product title
- ❌ Megamenu closes when hovering over dropdown content (must stay open when hovering trigger OR dropdown)
-**CRITICAL: Megamenu positioning errors** - Three common mistakes:
- ❌ Navbar doesn't have `position: relative` (megamenu won't position correctly)
- ❌ Megamenu positioned relative to trigger button instead of navbar (use `absolute left-0` on megamenu)
- ❌ Megamenu doesn't span full width (must use `right-0` or `w-full`, not just `w-auto`)
- ❌ Hardcoding categories, featured products, or any dynamic content instead of fetching from backend
- ❌ No clear indication of current page in category navigation
**2. Product Browsing Mistakes**
- ❌ Creating static routes for products/categories (use dynamic routes like `/products/[handle]` instead of `/products/shirt.tsx`)
- ❌ Missing "no products found" empty state with helpful suggestions
- ❌ No loading indicators while fetching products
- ❌ Pagination without SEO-friendly URLs (for search engines)
- ❌ Filter selections that don't persist on page reload
**3. Product Details Mistakes**
- ❌ Enabling "Add to Cart" before variant selection (size, color, etc.)
- ❌ Missing product images optimization (large uncompressed images)
- ❌ Navigating away from product page after adding to cart (stay on page)
- ❌ Using emojis in UI instead of icons or images (unprofessional, accessibility issues)
**4. Design and Consistency Mistakes**
-**CRITICAL: Not loading `reference/design.md` before creating ANY UI component** - Leads to inconsistent colors, fonts, and styles
- ❌ Introducing new colors without checking existing theme first
- ❌ Adding new fonts without verifying what's already used
- ❌ Using arbitrary Tailwind values when theme tokens exist
- ❌ Not detecting Tailwind version (v3 vs v4) - Causes syntax errors
**5. Checkout and Conversion Mistakes**
- ❌ Requiring account creation to checkout (offer guest checkout if backend supports it)
- ❌ Not fetching payment methods from backend - assuming available payment options or skipping payment method selection
- ❌ Overly complex multi-step checkout (4+ steps kills conversion) - Optimal is 3 steps: Shipping Info, Delivery Method + Payment, Review
- ❌ Missing trust signals (secure checkout badge, return policy link)
- ❌ Not handling out-of-stock errors gracefully during checkout
**6. Mobile Experience Mistakes**
- ❌ Touch targets smaller than 44x44px (buttons, links, form fields)
- ❌ Desktop-style hover menus on mobile (use tap/click instead)
- ❌ Not optimizing images for mobile (loading huge desktop images)
- ❌ Missing mobile-specific patterns (bottom nav, drawer filters)
**7. Performance and SEO Mistakes**
- ❌ Missing structured data (Product schema) for SEO
- ❌ No explicit image lazy loading (don't assume browser defaults) - Always add `loading="lazy"` to images below the fold
- ❌ Missing meta tags and Open Graph for social sharing
- ❌ Not optimizing Core Web Vitals (LCP, FID, CLS) - Use [PageSpeed Insights](https://pagespeed.web.dev/) or Lighthouse to measure
**8. Backend Integration Mistakes**
-**ERROR: Writing code that calls backend APIs/SDKs without following the 5-step verification workflow** - You MUST: 1) PAUSE, 2) QUERY docs/MCP, 3) VERIFY with user, 4) Write code, 5) CHECK for type errors
-**ERROR: Ignoring TypeScript errors on SDK methods** - Type errors mean you used wrong method names or parameters. Go back and verify with docs/MCP
-**ERROR: Guessing API method names, SDK methods, or parameters** - Always verify exact method signatures before use
-**ERROR: Not using Medusa MCP server when available** - If using Medusa backend, always query MCP server for methods
-**ERROR: Copying code examples without verifying they're current** - Examples may be outdated, always verify first
- ❌ Not detecting which backend is being used (check monorepo, ask user if unsure)
- ❌ Assuming API structure without checking backend documentation or MCP server
- ❌ Hardcoding dynamic content (categories, regions, products, etc.) instead of fetching from backend
- ❌ Defining custom types for Medusa entities instead of using `@medusajs/types` package
- ❌ Initializing Medusa SDK without publishable API key (required for multi-region stores and product pricing)
- ❌ Fetching Medusa products without passing `region_id` query parameter (causes missing or incorrect pricing)
- ❌ Showing all countries in Medusa checkout - should only show countries from cart's region
- ❌ Dividing Medusa prices by 100 (Medusa stores prices as-is, not in cents like Stripe)
- ❌ Missing Vite SSR config for Medusa SDK (add `ssr.noExternal: ['@medusajs/js-sdk']` to vite.config.ts)
- ❌ Running Medusa storefront on port other than 8000 (causes CORS errors - Medusa backend expects port 8000 by default)
- ❌ Not handling loading, error, and empty states for API calls
- ❌ Making API calls on client-side that should be server-side (SEO, security)
- ❌ Not implementing proper error messages ("Error occurred" vs "Product out of stock")
- ❌ Missing cache invalidation (stale product data, prices, inventory)
-**Not clearing cart state after order is placed** - Cart popup shows old items because cart wasn't reset from Context/localStorage/cache

View File

@@ -0,0 +1,123 @@
# Breadcrumbs Component
## Contents
- [Overview](#overview)
- [When to Use Breadcrumbs](#when-to-use-breadcrumbs)
- [Ecommerce Breadcrumb Patterns](#ecommerce-breadcrumb-patterns)
- [Mobile Breadcrumbs](#mobile-breadcrumbs)
- [SEO Structured Data](#seo-structured-data)
- [Checklist](#checklist)
## Overview
Breadcrumbs show the user's location within the site hierarchy (Home → Category → Subcategory → Product). Critical for ecommerce navigation and SEO.
**Assumed knowledge**: AI agents know how to build breadcrumbs with separators and links. This guide focuses on ecommerce-specific patterns.
### Key Requirements
- Show full path from homepage to current page
- Each level clickable (except current page)
- Position below navbar, above page title
- Include structured data for SEO (JSON-LD)
- Mobile-optimized (back link pattern)
## When to Use Breadcrumbs
**Use for:**
- Product pages (Home → Category → Subcategory → Product)
- Category pages (Home → Category → Subcategory)
- Deep site hierarchies (3+ levels)
- Large catalogs with many categories
**Don't use for:**
- Homepage (no parent pages)
- Flat site structures (1-2 levels)
- Checkout flow (linear, not hierarchical)
- Search results (not hierarchical)
## Ecommerce Breadcrumb Patterns
### Product Page Breadcrumbs
**Standard pattern:**
- Home / Category / Subcategory / Product Name
- Example: Home / Electronics / Laptops / Gaming Laptop Pro
**Key considerations:**
- All levels except product name are clickable
- Product name is current page (non-clickable, darker text)
- Shows product's location in catalog
**Multiple category membership:**
- If product in multiple categories, choose primary/canonical
- Match category in URL or navigation path
- Be consistent across site
### Category Page Breadcrumbs
**Standard pattern:**
- Home / Parent Category / Current Category
- Example: Home / Electronics / Laptops
**Current category:**
- Non-clickable (plain text)
- Visually distinct from links (darker or bold)
### Path Construction
**Hierarchy:**
- Start with "Home" (or home icon)
- Follow category hierarchy
- End with current page
- Maximum 5-6 levels (keep shallow)
**URL alignment:**
- Breadcrumb path should match URL hierarchy
- Consistent naming between URLs and breadcrumbs
- Example: `/categories/electronics/laptops` → "Home / Electronics / Laptops"
## Mobile Breadcrumbs
### Mobile Pattern: Collapse to Back Link
**Recommended approach:**
- Show only previous level as back link
- Back arrow icon (←) + parent page name
- Example: "← Gaming Laptops"
**Why:**
- Saves vertical space on mobile
- Clear affordance (back navigation)
- Simpler than full breadcrumb trail
- Mobile users have device back button
**Alternative: Truncated path**
- Show "Home ... Current Page"
- Hide middle levels
- Balances space and context
## SEO Structured Data
**BreadcrumbList schema (CRITICAL)**: Add JSON-LD structured data. Breadcrumbs appear in search results, improves CTR, helps search engines understand site structure.
**Implementation**: schema.org BreadcrumbList with items array. Each item has position (1, 2, 3...), name, and URL. See seo.md for schema details.
## Checklist
**Essential features:**
- [ ] Positioned below navbar, above page title
- [ ] Full path shown (Home → Category → Product)
- [ ] All levels clickable except current page
- [ ] Current page visually distinct (non-clickable, darker)
- [ ] Clear separators (, /, > or chevron)
- [ ] Mobile: Back link pattern ("← Category")
- [ ] Structured data (JSON-LD BreadcrumbList)
- [ ] Semantic HTML (`<nav aria-label="Breadcrumb">`)
- [ ] `aria-current="page"` on current item
- [ ] Keyboard accessible (tab through links)
- [ ] Truncate long labels (20-30 characters max)
- [ ] Consistent with navigation labels
- [ ] Maximum 5-6 levels deep

View File

@@ -0,0 +1,189 @@
# Cart Popup Component
## Contents
- [Overview](#overview)
- [When to Show Cart Popup](#when-to-show-cart-popup)
- [Layout Patterns](#layout-patterns)
- [Cart Display](#cart-display)
- [Actions and CTAs](#actions-and-ctas)
- [Empty State](#empty-state)
- [Mobile Considerations](#mobile-considerations)
- [Checklist](#checklist)
## Overview
Cart popup (mini cart/cart drawer) shows quick cart overview without navigating away. Opens from cart icon click or after adding items.
**⚠️ CRITICAL: Always display variant details (size, color, material, etc.) in cart popup, not just product titles.**
**Assumed knowledge**: AI agents know how to build modals, dialogs, and overlays. This focuses on ecommerce-specific patterns.
**Cart popup vs full cart page:**
- Popup: Quick overview, fast checkout path, continue shopping easily
- Full page: Detailed review, promo codes, complex operations
- **Recommended**: Both - popup for speed, full cart page for details
## When to Show Cart Popup
**Trigger options:**
1. **On cart icon click** (always) - Click cart icon in navbar opens popup
2. **After adding to cart** (recommended) - Auto-open popup when item added, confirms action, allows checkout or continue shopping
3. **Hover cart icon** (desktop only, optional) - Quick peek on hover. Can be accidental, not recommended.
**Add-to-cart feedback alternatives:**
- Show popup (most common) - Immediate confirmation, clear path to checkout
- Toast only (less intrusive) - Small notification, user clicks cart icon to see details
- Navigate to cart page (traditional) - Goes directly to full cart page, less common now
## Layout Patterns
**Two common patterns:**
**1. Dropdown (recommended for simplicity):**
- Drops down from cart icon, positioned below navbar
- Width: 280-320px, max height with scroll
- No backdrop overlay (click outside to close)
- Better for few items, simpler implementation
**2. Slide-in drawer (more prominent):**
- Slides from right, full height, width 320-400px (desktop) or 80-90% (mobile)
- Semi-transparent backdrop overlay (click to close)
- Better for multiple items or complex carts
**Both patterns have:**
- Header: Title + item count + close button (optional for dropdown)
- Scrollable content: List of cart items
- Sticky footer: Subtotal + action buttons (Checkout, View Cart)
## Cart Display
**Fetch cart data from backend:**
- Cart ID from localStorage
- Line items (products, variants, quantities, prices)
- Cart totals (subtotal, tax, shipping)
- See connecting-to-backend.md for backend integration
**When to fetch:**
- On app initialization (update cart icon badge)
- On popup open (show loading state)
- After cart updates (add/remove/change quantity)
**State management:**
- Store cart data globally (React Context or TanStack Query)
- Persist cart ID in localStorage
- Optimistic UI updates (update immediately, revert on error)
- **CRITICAL: Clear cart state after order is placed** - See connecting-to-backend.md for cart cleanup pattern
- Common issue: Cart popup shows old items after checkout because cart state wasn't cleared
- See connecting-to-backend.md for cart state patterns
**Cart item display:**
**CRITICAL: Always show variant details (size, color, material, etc.) for each cart item.**
Without variant details, users can't confirm they added the correct variant. This is especially critical when products have multiple options.
- Product image (60-80px thumbnail)
- Product title (truncated to 2 lines)
- **Variant details (REQUIRED)**: Size, color, material, or other variant options
- Format: "Size: Large, Color: Black" or "Large / Black"
- Show ALL selected variant options, not just product title
- Display below title, smaller text (gray)
- Quantity controls (+/- buttons, debounce 300-500ms)
- Unit price and total price (line item total = price × quantity)
- Remove button (X icon, no confirmation needed)
**Why variant details are critical:**
- User confirmation: "Did I add the right size?"
- Prevents cart abandonment from uncertainty
- Allows corrections before checkout
- Essential for products with multiple variants (clothing, shoes, configurable products)
## Actions and CTAs
**Cart summary display:**
- Subtotal (sum of all items)
- Shipping and tax: "Calculated at checkout" or actual amount
- Total: Bold and prominent
**Free shipping indicator (optional):**
- "Add $25 more for free shipping" with progress bar
- Encourages larger orders, updates as cart changes
**Promo codes:**
- Usually NOT in cart popup (too cramped)
- Reserve for full cart page
- Exception: Simple code input if space permits
**Action buttons:**
1. **Checkout** (primary) - Most prominent, high contrast (brand color), navigates to checkout
2. **View Cart** (secondary) - Outline or subtle, navigates to full cart page
Both buttons full-width, 44-48px height on mobile.
## Empty State
Show icon/illustration + "Your cart is empty" + "Continue Shopping" button. Centered, friendly, minimal design.
## Loading and Error States
**On popup open**: Show skeleton/placeholder while fetching (avoid blank screen)
**During updates**:
- Quantity changes: Inline spinner, disable controls, debounce 300-500ms
- Item removal: Fade out animation, disable remove button during request
- Add to cart: Loading indicator on button ("Adding...")
**Error handling**:
- Network errors: Show retry option, don't close popup
- Invalid cart ID: Create new cart automatically
- Out of stock: Disable quantity increase, show message
- Revert optimistic updates on failure
**Animations**: Smooth transitions (250-350ms), slide-in drawer, backdrop fade-in/out. Highlight newly added items.
## Mobile Considerations
**Dropdown on mobile:**
- Full-width (100% minus margins)
- Max height 60-70% viewport, scrollable
- Tap outside to close
**Drawer on mobile:**
- 85-95% screen width or full screen
- Slides from right or bottom
- Swipe to close gesture supported
- Backdrop overlay
**Mobile adjustments:**
- Large touch targets (44-48px minimum)
- Full-width action buttons (48-52px height)
- Smaller images (60px), truncate titles
- Sticky footer with actions
- Large close button (44x44px)
## Checklist
**Essential features:**
- [ ] Opens on cart icon click
- [ ] Dropdown (280-320px) or drawer (320-400px) layout
- [ ] Close button or click outside to close
- [ ] Backdrop overlay if drawer
- [ ] **CRITICAL: Cart items show variant details (size, color, etc.) - not just product title**
- [ ] Cart items with image, title, variant options, quantity, prices
- [ ] Quantity controls (+/- buttons, debounced)
- [ ] Remove item button
- [ ] Subtotal displayed
- [ ] Checkout button (primary)
- [ ] View Cart button (secondary)
- [ ] Empty state with "Continue Shopping" CTA
- [ ] Loading states (skeleton/spinner)
- [ ] Smooth animations (250-350ms)
- [ ] Mobile: Full-width dropdown or 85-95% drawer
- [ ] Touch targets 44-48px minimum
- [ ] `role="dialog"` and `aria-modal="true"`
- [ ] ARIA labels on cart button ("Shopping cart with 3 items")
- [ ] Keyboard accessible (focus trap, Escape closes, return focus)
- [ ] Screen reader announcements (item added/removed)
- [ ] Real-time cart count badge updates

View File

@@ -0,0 +1,298 @@
# Country Selector Component
## Contents
- [Overview](#overview)
- [When to Implement](#when-to-implement)
- [UI Patterns](#ui-patterns)
- [State Management](#state-management)
- [Backend Integration](#backend-integration)
- [Detection and Defaults](#detection-and-defaults)
- [Mobile Considerations](#mobile-considerations)
- [Checklist](#checklist)
## Overview
Country selector allows customers to choose their country/region, which determines currency, pricing, available products, shipping options, payment methods, and localized content.
### Key Ecommerce Functions
- Display prices in correct currency
- Show country-specific product availability
- Apply region-specific promotions and discounts
- Calculate accurate shipping costs and delivery times
- Enable appropriate payment methods
- Display localized content and language
### Purpose
**Why country/region selection matters:**
- Prices vary by region (currency, taxes, import fees)
- Product availability differs by market
- Shipping methods and costs are region-specific
- Legal requirements vary (privacy, consumer protection)
- Payment methods differ by country
- Improves user experience with relevant content
## When to Implement
**Implement country selector when:**
- Backend supports multiple countries or regions
- Selling to multiple countries or regions
- Prices vary by location (currency, taxes)
- International shipping with different rates
- Region-specific product catalogs
- Multi-currency support needed
- Legal or regulatory requirements vary by region
**Skip if:**
- Backend doesn't support multiple countries or regions
- All prices in one currency
- No regional differences in catalog or pricing
## UI Patterns
### Placement Options
**Footer placement (modern and minimal):**
- Bottom of page in footer
- Less prominent but always accessible
- Icon (flag or globe) + country code/name
**Header placement (most common):**
- Top-right of navigation bar
- Icon (flag or globe) + country code/name
- Click opens dropdown or modal selector
**Modal/popup on first visit:**
- Detect location and suggest country
- Allow user to confirm or change
- Store preference for future visits
### Selector Design Patterns
**Pattern 1: Dropdown (Recommended)**
Small, compact selector in header. Shows current country flag/name, click to open dropdown with country list.
**Pros:** Doesn't interrupt browsing, always accessible, familiar pattern.
**Pattern 2: Modal on First Visit**
Full-screen or centered modal on first visit. "Select your country to see accurate prices and shipping."
**Pros:** Forces initial selection, ensures accurate pricing from start.
**Cons:** Can be intrusive, delays browsing.
**Tradeoff:** Modal ensures selection but adds friction. Dropdown is less intrusive but users may miss it.
**Pattern 3: Inline Banner**
Sticky banner at top: "Shipping to United States? Change" with link to selector.
**Pros:** Non-intrusive reminder, doesn't block content.
**Cons:** Takes vertical space, easy to ignore.
### Country List Display
**Search + list:**
- Search input at top
- Alphabetical country list below
- Popular countries at top (US, UK, Canada, etc.)
- Flag icons for visual recognition
**Grouped by region:**
- North America, Europe, Asia, etc.
- Collapsible sections
- Helpful for large lists (100+ countries)
**Format:**
```
🇺🇸 United States (USD)
🇬🇧 United Kingdom (GBP)
🇨🇦 Canada (CAD)
───────────────────
🇩🇪 Germany (EUR)
🇫🇷 France (EUR)
```
Show flag, country name, and currency code for clarity.
## State Management
### Storing Country Selection
**Client-side storage (recommended):**
- localStorage or cookies
- Persists across sessions
- Key: `region_id` or `country_code`
**Why local storage:**
- Fast access without API call
- Available immediately on page load
- No server round-trip needed
### Context Provider Pattern
**Recommended: Create context for region/country data.**
Provides quick access throughout the app to:
- Selected country
- Selected region (if applicable)
- Currency
- Available payment methods
- Shipping options
**Benefits:**
- Centralized country/region logic
- Easy access from any component
- Single source of truth
- Simplified cart and product queries
**Example structure:**
```typescript
interface RegionContext {
country: string
region?: string
currency: string
changeCountry: (country: string) => void
}
```
### When to Apply Selection
**Apply country/region to:**
- Product price display (convert currency, apply regional pricing)
- Cart creation (set region for accurate totals)
- Product queries (retrieve accurate pricing)
- Checkout flow (shipping methods, payment options)
- Content display (language, measurements)
## Backend Integration
### General Backend Requirements
**What backend needs to provide:**
- List of available countries/regions
- Mapping of countries to regions (if using regional structure)
- Pricing per region or country
- Product availability by region
- Shipping methods by region
- Supported payment methods by region
**API considerations:**
- Fetch country/region list on app load
- Pass selected country/region to product queries
- Include region in cart creation
- Validate country selection on backend
### Medusa Backend Integration
**For Medusa users, regions are critical for accurate pricing.**
Medusa uses regions (not individual countries) for pricing. A region can contain multiple countries.
**Key concepts:**
- **Region**: Group of countries with shared pricing (e.g., "Europe" region)
- **Country**: Individual country within a region
- **Currency**: Each region has one currency
**Mapping country to region:**
1. Customer selects country (e.g., "Germany")
2. Find which region contains that country (e.g., "Europe" region)
3. Store region ID for cart and product operations
4. Use region for all pricing queries
**Required for:**
- Creating carts: Must pass region ID
- Retrieving products: Pass region to get accurate prices
- Product availability: Products may be region-specific
**Implementation pattern:**
Create a context that stores both country and region. When country changes, look up corresponding region and update both.
**For detailed Medusa region implementation, see:**
- Medusa storefront regions documentation: https://docs.medusajs.com/resources/storefront-development/regions/context
- Medusa JS SDK regions endpoints
- Consult Medusa MCP server for real-time API details
**Other backends:**
Check the ecommerce backend's documentation for country/region handling patterns.
## Detection and Defaults
### Auto-Detection
**IP-based geolocation (recommended):**
Detect user's country from IP address. Use as default but allow user to change.
**Implementation:**
- Use geolocation API or service (MaxMind, ipapi.co, CloudFlare)
- Server-side detection (more accurate)
- Set as default, show confirmation: "Shipping to United States?"
**Benefits:** Reduces friction, most users keep detected country.
**Tradeoff:** Not 100% accurate (VPNs, proxies). Always allow manual override.
### Fallback Strategy
**If detection fails or unavailable:**
1. Check localStorage for previous selection
2. Use browser language as hint (`navigator.language`)
3. Default to primary market (e.g., US for US-based store)
4. Prompt user to select on first interaction (cart, checkout)
**Never block browsing if country unknown.**
Allow browsing with default pricing, prompt selection before checkout.
## Mobile Considerations
**Selector placement:**
Mobile hamburger menu or bottom of page. Top-right in mobile header if space allows.
**Modal selector:**
Full-screen modal on mobile for country selection. Large touch targets (48px), search input at top, easy scrolling.
**Sticky reminder:**
Small banner: "Shipping to US? Change" with tap to open selector.
**Detection prompt:**
Bottom sheet: "We detected you're in Germany. Is this correct?" with Confirm/Change buttons.
## Checklist
**Essential features:**
- [ ] Country selector visible (header, footer, or first-visit modal)
- [ ] Current country clearly displayed (flag, name, currency)
- [ ] Dropdown or modal with country list
- [ ] Search functionality for long country lists
- [ ] Popular countries at top of list
- [ ] Flag icons for visual recognition
- [ ] Show currency code per country
- [ ] localStorage persistence (save selection)
- [ ] Context provider for region/country data
- [ ] Auto-detection based on IP (optional)
- [ ] Manual override always available
- [ ] Apply to product prices (currency, regional pricing)
- [ ] Apply to cart creation (set region)
- [ ] Apply to checkout (shipping, payment methods)
- [ ] Fallback if detection fails
- [ ] Mobile: Full-screen modal or bottom sheet
- [ ] Mobile: Large touch targets (48px)
- [ ] Backend integration (fetch regions, map countries)
- [ ] For Medusa: Region context with country-to-region mapping
- [ ] For Medusa: Pass region to cart and product queries
- [ ] ARIA label on selector button
- [ ] Keyboard accessible (Tab, Enter, arrows)
- [ ] Screen reader announces country changes
**Optional enhancements:**
- [ ] Currency conversion display (show original + converted)
- [ ] Language selector tied to country
- [ ] Shipping estimate based on country
- [ ] Tax estimation display
- [ ] Regional content (images, messaging)
- [ ] "Not shipping to your country?" alternative

View File

@@ -0,0 +1,112 @@
# Footer Component
## Contents
- [Overview](#overview)
- [Essential Footer Elements](#essential-footer-elements)
- [Dynamic Category Links (Ecommerce-Specific)](#dynamic-category-links-ecommerce-specific)
- [Newsletter Signup](#newsletter-signup)
- [Payment and Trust Badges](#payment-and-trust-badges)
- [Mobile Footer](#mobile-footer)
## Overview
Footer provides supplementary navigation, company info, and trust signals. Appears on every page.
**Assumed knowledge**: AI agents know how to build multi-column layouts and navigation lists. This guide focuses on ecommerce footer patterns.
### Key Requirements
- Navigation links (categories, pages)
- Dynamic category fetching from backend
- Legal links (Privacy, Terms)
- Newsletter signup
- Payment method badges
- Social media links
- Responsive (multi-column desktop, single-column mobile)
## Essential Footer Elements
### Must-Have Content
**Required:**
- Navigation links (categories from backend)
- Contact information (email, phone)
- Legal links (Privacy Policy, Terms of Service)
- Copyright notice with current year
**Strongly recommended:**
- Newsletter signup form
- Payment method badges
- Social media links
- Trust signals
### Multi-Column Layout (Desktop)
**Standard pattern: 4-5 columns**
- Column 1: Shop/Categories (dynamic from backend)
- Column 2: Customer Service (Contact, FAQ, Shipping)
- Column 3: Company (About, Careers)
- Column 4: Newsletter signup
- Bottom: Legal links, payment badges, copyright
## Dynamic Category Links (Ecommerce-Specific)
**CRITICAL: Fetch categories from backend dynamically** - never hardcode. Fetch from ecommerce backend API (for Medusa: `sdk.store.category.list()`).
**Benefits:**
- Stays in sync with main navigation
- Categories added/removed automatically
- No manual footer updates
**Guidelines:**
- Show top-level categories only (5-8 max)
- Match labels from main navigation
- Cache category data (rarely changes)
## Newsletter Signup
**Essential elements:**
- Email input + submit button ("Subscribe")
- **Value proposition (CRITICAL)**: State clear benefit ("Get 10% off your first order", "Exclusive deals + early access"). Don't just say "Subscribe to newsletter".
- Privacy note: "We respect your privacy" + link to privacy policy
**Layout:** Input + button inline (desktop), stacked (mobile). Full width on mobile.
## Payment and Trust Badges
**Payment method icons:**
Display accepted payments (Visa, Mastercard, PayPal, Apple Pay, Google Pay). 40-50px icons, horizontal row, bottom of footer.
**Trust badges (optional):**
Max 3-4 legitimate certifications (SSL, BBB, money-back guarantee). Only use real badges with verification links.
## Mobile Footer
**Single column, stacked:** Logo → Navigation → Newsletter → Social → Legal/copyright.
**Collapsible sections (optional):** Accordion pattern for navigation to reduce height. Keep newsletter/social always visible.
**Touch-friendly:** 44px minimum links, 8-12px spacing, 14-16px text, 48px newsletter input height.
## Checklist
**Essential features:**
- [ ] Navigation links (categories, pages)
- [ ] Categories fetched dynamically from backend
- [ ] Contact information (email, phone)
- [ ] Legal links (Privacy Policy, Terms of Service)
- [ ] Copyright notice with current year
- [ ] Newsletter signup form with value proposition
- [ ] Payment method icons
- [ ] Social media links
- [ ] Responsive (4-5 columns desktop, single-column mobile)
- [ ] Mobile: 44px touch targets
- [ ] Mobile: Collapsible sections (optional)
- [ ] Semantic HTML (`<footer>`, `<nav>` sections)
- [ ] ARIA labels on navigation ("Footer navigation")
- [ ] Keyboard accessible
- [ ] Visible focus indicators
- [ ] Color contrast 4.5:1 minimum
- [ ] Consistent across all pages

View File

@@ -0,0 +1,241 @@
# Hero Section Component
## Contents
- [Overview](#overview)
- [Hero Types and When to Use](#hero-types-and-when-to-use)
- [Content Guidelines](#content-guidelines)
- [Multiple Heroes (Carousel)](#multiple-heroes-carousel)
- [Mobile Hero](#mobile-hero)
- [Performance](#performance)
- [Checklist](#checklist)
## Overview
Hero section is the prominent banner at top of homepage, immediately below navigation. First content users see - sets tone for shopping experience.
**Assumed knowledge**: AI agents know how to build full-width banners with images and text overlays. This focuses on ecommerce hero patterns.
**Key requirements:**
- Above the fold (immediately visible)
- Clear value proposition or promotional message
- High-quality imagery
- Strong call-to-action
- Fast loading (critical for first impression)
## Hero Types and When to Use
### 1. Full-Width Banner (Most Common)
**Characteristics:**
- Spans entire viewport width
- Large background image or video
- Text overlay with headline + CTA
- Single focused message
**Best for:**
- Seasonal campaigns ("Summer Sale")
- New product arrivals
- Brand storytelling
- Single promotional focus
- Simple, bold message
**Example:** Background image of products, headline "40% Off Summer Sale", CTA "Shop Now"
### 2. Split Hero (Image + Content)
**Characteristics:**
- 50/50 or 60/40 split layout
- Image on one side, text content on other
- No text overlay on image
- Cleaner, easier to read
**Best for:**
- Product launches (show product clearly)
- Detailed messaging (more text space)
- Accessibility (no text-on-image contrast issues)
- Professional/B2B stores
**Example:** Product image (left 50%), headline + benefits + CTA (right 50%)
### 3. Minimal Hero
**Characteristics:**
- Large image, minimal text
- Image does storytelling
- Subtle headline, small CTA
- Emphasis on visual brand
**Best for:**
- Luxury brands (sophisticated aesthetic)
- Lifestyle brands (aspirational imagery)
- Photography-focused products
- Brand over promotion
### 4. Video Hero
**Characteristics:**
- Background video (muted, looping)
- Text overlay on video
- Fallback image for slow connections
**Best for:**
- Fashion brands (show products in motion)
- Lifestyle products (demonstrate usage)
- High-budget campaigns
- Brand storytelling with motion
**Important:** Auto-play muted, provide play/pause controls, optimize file size (<5MB), use poster image fallback.
### 5. Product Showcase Hero
**Characteristics:**
- Multiple featured products in hero
- Grid of 2-4 products
- Quick links to product pages
- Less promotional, more discovery
**Best for:**
- Multi-category stores
- Product-focused (not campaign-focused)
- Quick product discovery
- Minimal marketing, maximum browsing
## Content Guidelines
**Headline best practices:**
- Short and impactful (5-10 words)
- Clear value proposition ("Free Shipping on All Orders")
- Urgency if time-sensitive ("48-Hour Flash Sale")
- Benefit-focused ("Upgrade Your Style")
- Avoid generic ("Welcome to Our Store")
**Subtext (optional):**
- 10-20 words maximum
- Expand on headline benefit
- Add context or details
- Not always necessary (clean design)
**Call-to-action:**
- Single primary CTA button
- Action-oriented text ("Shop Now", "Explore Category", "Get Started")
- High contrast (stands out on image)
- Large enough (48px height minimum)
- Link to relevant landing page (sale, category, product listing)
**Image selection:**
- High quality, professional photography
- Shows products or lifestyle context
- Represents brand aesthetic
- Optimized for web (<500KB)
- Responsive (different crops for mobile)
- Ensure text overlay is readable (adequate contrast)
## Multiple Heroes (Carousel)
**Carousel pattern:**
- 2-4 slides rotating automatically
- Each slide = independent hero (own message, image, CTA)
- Auto-rotate every 5-7 seconds (slow enough to read)
- Manual controls (prev/next arrows, dot indicators)
- Pause on hover (accessibility)
**When to use carousel:**
- Multiple concurrent campaigns (Winter Sale + New Arrivals)
- Different audience segments (Men/Women/Kids)
- Seasonal variety showcase
- Limited above-fold space
**When NOT to use carousel:**
- Single focused campaign (just use one hero)
- Users rarely see slides beyond first (carousel blindness)
- Slower page load (multiple images)
- Accessibility concerns (auto-rotating content)
**Carousel best practices:**
- Max 3-4 slides (more = ignored)
- First slide most important (most viewed)
- Consistent layout across slides
- Clear indicators showing progress
- Don't rely on later slides for critical info
- Pause on interaction (hover, focus)
## Mobile Hero
**Mobile adjustments (CRITICAL):**
**Layout:**
- Full-width, portrait aspect (2:3 or 3:4)
- Vertical composition (text overlays center/bottom)
- Larger text for readability
- Simplified message (shorter headline)
**Split hero on mobile:**
- Stack vertically (image top, text bottom)
- Don't use side-by-side (too cramped)
**Performance:**
- Smaller images (<300KB)
- Different image crop for mobile portrait
- Use `srcset` or `<picture>` for responsive images
- Consider static image instead of video (mobile data)
**Touch interactions:**
- Large CTA button (48px height minimum)
- Easy carousel controls (if used)
- Swipe gesture for carousel slides
## Performance
**Critical for first impression:**
**Image optimization:**
- WebP format with JPEG fallback
- Lazy load below-fold content (not hero - it's above fold)
- Responsive images (mobile gets smaller size)
- Target: <500KB desktop, <300KB mobile
- Use CDN for faster delivery
**Video optimization:**
- <5MB file size maximum
- Muted, autoplay, loop
- Poster image (shows before video loads)
- Fallback to image on slow connections
- Consider not using on mobile (data/performance)
**LCP optimization:**
- Hero image is often Largest Contentful Paint
- Preload hero image: `<link rel="preload" as="image" href="hero.jpg">`
- Inline critical CSS for hero
- Avoid layout shift (set image dimensions)
**Target metrics:**
- LCP < 2.5 seconds
- No layout shift (CLS < 0.1)
- Fast interaction (hero CTA clickable immediately)
## Checklist
**Essential features:**
- [ ] Above the fold (immediately visible)
- [ ] Clear headline (5-10 words, value proposition)
- [ ] High-quality image (professional, on-brand)
- [ ] Primary CTA button (action-oriented, high contrast)
- [ ] Fast loading (<500KB image desktop, <300KB mobile)
- [ ] Responsive images (different sizes/crops for devices)
- [ ] Mobile: Portrait aspect ratio (2:3 or 3:4)
- [ ] Mobile: Vertical text placement (center/bottom)
- [ ] Mobile: Large CTA (48px height minimum)
- [ ] Text overlay readable (adequate contrast, background overlay)
- [ ] If carousel: Max 3-4 slides
- [ ] If carousel: Auto-rotate 5-7 seconds
- [ ] If carousel: Pause on hover/focus
- [ ] If carousel: Manual controls (arrows, dots)
- [ ] If video: Muted, autoplay, loop
- [ ] If video: Poster image fallback
- [ ] If video: <5MB file size
- [ ] Preload hero image (LCP optimization)
- [ ] No layout shift (set image dimensions)
- [ ] ARIA labels on carousel controls
- [ ] Keyboard accessible (Tab to CTA, arrow keys for carousel)

View File

@@ -0,0 +1,239 @@
# Megamenu Component
## Contents
- [Overview](#overview)
- [When to Use Megamenu](#when-to-use-megamenu)
- [Content Organization](#content-organization)
- [Layout Patterns](#layout-patterns)
- [Trigger Behavior](#trigger-behavior)
- [Mobile Alternative](#mobile-alternative)
- [Checklist](#checklist)
## Overview
Megamenu is a large, full-width dropdown navigation showing multiple columns of categories, links, and promotional content. Opens from navbar trigger items (e.g., "Shop", "Men", "Women").
**Assumed knowledge**: AI agents know how to build dropdown menus with hover/click triggers. This focuses on ecommerce megamenu patterns.
**Key requirements:**
- Full-width display (spans viewport)
- Multiple columns for categories
- Positioned directly below navbar
- Optional promotional images
- Mobile alternative (hamburger menu, not megamenu)
## When to Use Megamenu
**Use megamenu when:**
- Large product catalog (10+ top-level categories)
- Deep hierarchy (parent → child → grandchild levels)
- Want to showcase featured products/campaigns
- Multiple segments (Men, Women, Kids, etc.)
- Visual storytelling needed
**Use simple dropdown when:**
- Small catalog (<10 categories)
- Flat category structure (1-2 levels)
- Text-only navigation sufficient
- Minimalist design preference
**Common megamenu triggers:**
- "Shop" (all categories)
- "Men", "Women", "Kids" (segmented)
- "New Arrivals" (curated)
- "Sale" (promotional)
## Content Organization
**Backend Integration (CRITICAL):**
Fetch categories dynamically from ecommerce backend - never hardcode categories. Categories change frequently (new products, seasonal updates, inventory changes). Fetch from API on component mount or during SSR.
**Column structure (3-5 columns recommended):**
**Column 1-3: Category columns**
- Parent category header (bold, non-clickable or clickable to "View All")
- Child categories below (clickable links)
- 5-10 links per column maximum
- Group related subcategories
**Example:**
```plaintext
Electronics (header)
Laptops
Desktops
Monitors
Accessories
View All Electronics
```
**Column 4-5: Promotional/Featured**
- Product image card (1-2 featured products)
- Campaign banner ("Summer Sale", "New Arrivals")
- "Shop the Look" curated sets
- Seasonal promotions
**Content limits:**
- Max 5 columns (avoid overcrowding)
- Max 10 links per column
- 1-2 promotional images maximum
- Keep height reasonable (<600px)
## Layout Patterns
### ⚠️ CRITICAL: Megamenu Positioning (Common Mistake)
**Common positioning errors that MUST be avoided:**
**Mistake 1: Navbar doesn't have `position: relative`**
- Without positioning context on navbar, megamenu won't position correctly
- Megamenu will position relative to document body instead of navbar
**Mistake 2: Megamenu positioned relative to trigger button**
- Causes megamenu to appear offset, not aligned to left edge
- Megamenu won't span full width of navbar
- Different trigger positions cause inconsistent megamenu placement
**Mistake 3: Megamenu doesn't span full width**
- Using `width: auto` or no width constraint
- Missing `left: 0` and `right: 0` properties
- Results in narrow dropdown instead of full-width panel
---
**REQUIRED positioning pattern:**
**Visual structure:**
```
┌─────────────────────────────────────────────────┐
│ NAVBAR (position: relative) │
│ [Logo] [Shop ▼] [Men] [Women] [Cart] │
└─────────────────────────────────────────────────┘
┌───────────────────────────────────────────────┐
│ MEGAMENU (absolute, left: 0, full width) │
│ ┌─────────────────────────────────────────┐ │
│ │ Container (centered content) │ │
│ │ [Col1] [Col2] [Col3] [Promo] │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
```
**Required structure:**
1. **Navbar container**
- MUST have `position: relative`
- Creates positioning context for megamenu
- Contains both trigger button and megamenu dropdown
2. **Megamenu dropdown**
- MUST have `position: absolute`
- MUST have `left: 0` (aligns to left edge of navbar)
- MUST have `right: 0` OR `width: 100%` (spans full navbar width)
- MUST have `top: 100%` (positioned directly below navbar)
- Should have appropriate `z-index` (above content, below modals)
3. **Content wrapper (inside megamenu)**
- Use constrained width container (e.g., `max-width`, `container`)
- Center content with `margin: 0 auto`
- Contains grid/columns for megamenu content
**Why this pattern is mandatory:**
- Navbar `position: relative` creates positioning context
- Megamenu `absolute` + `left: 0` + full width ensures consistent, full-width layout
- Positioning relative to navbar (not trigger) prevents offset issues
- Inner container centers content while maintaining full-width background
---
### Other Layout Considerations
- Positioned below navbar (no gap)
- White/light background, boxed padding
- Shadow or border for depth
- High z-index (above page content, below modals)
**Column layout:**
- Equal-width columns or flexible grid
- Adequate spacing (24-32px between columns)
- Left-aligned text in category columns
- Right column(s) for promotional content
- Responsive: Stack columns on tablet if needed
**Promotional images:**
- Right-aligned (1-2 columns)
- Aspect ratio: 2:3 or square
- Product images or lifestyle photography
- Clickable to product/category page
- Include caption or CTA ("Shop Now")
## Trigger Behavior
**Desktop hover (recommended):**
- Megamenu opens on trigger hover
- **CRITICAL: Megamenu MUST stay open while hovering over the dropdown content**
- Stays open while hovering trigger OR dropdown area
- Closes only when mouse leaves both trigger and dropdown areas
- Debounce close (200-300ms delay) to prevent accidental closure
- Smooth fade-in/out transition (200-300ms)
**Why this is critical:**
- If dropdown closes when moving from trigger to content, users can't access links
- Frustrating UX - users can't interact with megamenu items
- Common mistake: Only listening for hover on trigger, not on dropdown
**Desktop click (alternative):**
- Click trigger to toggle open/close
- Click outside to close
- Better for touch-enabled laptops
- Less accidental openings
**Hover flickering prevention:**
- No gap between navbar and dropdown
- Dropdown should slightly overlap navbar
- Debounce close delay prevents flickering
## Mobile Alternative
**Do NOT use megamenu on mobile:**
- Too large for mobile screens
- Hard to navigate multi-column layout
- Poor touch experience
**Mobile alternative (hamburger menu):**
- Hamburger icon opens slide-in drawer
- Vertical accordion for categories
- Parent category expands to show children
- Simple, scrollable list
- See navbar.md for mobile navigation patterns
**Breakpoint:**
- Megamenu: Desktop only (>1024px)
- Hamburger: Tablet and mobile (<1024px)
## Checklist
**Essential features:**
- [ ] Triggered from navbar items ("Shop", segments)
- [ ] **CRITICAL: Navbar container has `position: relative` (creates positioning context)**
- [ ] **CRITICAL: Megamenu has `position: absolute` with `left: 0` (NOT positioned relative to trigger button)**
- [ ] **CRITICAL: Megamenu spans full width (`right: 0` or `w-full`, NOT just `w-auto`)**
- [ ] **CRITICAL: Megamenu positioned at `top: 100%` or `top-full` (directly below navbar)**
- [ ] Full-width dropdown below navbar, spans entire navbar width
- [ ] 3-5 columns for organization
- [ ] Category hierarchy (parent → children links)
- [ ] Optional promotional images (1-2)
- [ ] **CRITICAL: Megamenu stays open when hovering over dropdown content (not just trigger)**
- [ ] Hover trigger with debounced close (200-300ms)
- [ ] Smooth fade-in/out transition
- [ ] No flickering (no gap between navbar and dropdown)
- [ ] Mobile: Use hamburger menu, NOT megamenu
- [ ] Keyboard accessible (Tab through links, Escape closes)
- [ ] `role="navigation"` on dropdown panel
- [ ] ARIA labels on trigger buttons
- [ ] Screen reader friendly (announce expand/collapse)
- [ ] Max 10 links per column
- [ ] Max 5 columns total
- [ ] Fetched dynamically from backend (don't hardcode categories)

View File

@@ -0,0 +1,397 @@
# Navbar Component
## Contents
- [Overview](#overview)
- [Decision: Simple Dropdown vs Megamenu](#decision-simple-dropdown-vs-megamenu)
- [Key Ecommerce Patterns](#key-ecommerce-patterns)
- [Layout Structure](#layout-structure)
- [Accessibility Essentials](#accessibility-essentials)
- [Common Ecommerce Mistakes](#common-ecommerce-mistakes)
- [Backend Integration](#backend-integration)
- [Checklist](#checklist)
## Overview
Primary navigation for ecommerce storefronts. Desktop: horizontal menu with category links. Mobile: hamburger drawer with accordion subcategories.
### ⚠️ CRITICAL: NEVER Hardcode Categories
**ALWAYS fetch categories dynamically from the backend. NEVER hardcode static category arrays.**
**WRONG - DO NOT DO THIS:**
```typescript
// WRONG - Static hardcoded categories
const categories = [
{ name: "Women", href: "/categories/women" },
{ name: "Men", href: "/categories/men" },
{ name: "Accessories", href: "/categories/accessories" }
]
```
**CORRECT - Fetch from backend:**
```typescript
// CORRECT - Fetch categories dynamically
const [categories, setCategories] = useState([])
useEffect(() => {
fetch(`${apiUrl}/store/product-categories`)
.then(res => res.json())
.then(data => setCategories(data.product_categories))
}, [])
```
**Why this matters:**
- Categories change frequently (new categories, renamed, reordered)
- Hardcoded categories become outdated immediately
- Requires code changes every time categories change
- Cannot scale to stores with dynamic catalogs
- Defeats the purpose of headless commerce
### Key Requirements
- Desktop: Horizontal category links, cart/account/search right-aligned
- Mobile: Hamburger drawer, cart stays visible in header (not hidden in drawer)
- **CRITICAL: Fetch categories from backend dynamically (NEVER hardcode static arrays)**
- Sticky: Recommended for easy cart access while browsing
- Real-time updates: Cart count, login state, category changes
## Decision: Simple Dropdown vs Megamenu
**Use Simple Dropdown when:**
- <10 top-level categories
- Flat or shallow hierarchy (1-2 levels deep)
- Minimal subcategories per parent
- Focused/specialized product catalog
**Use Megamenu when:**
- 10+ top-level categories
- Deep hierarchy (3+ levels)
- Need to showcase featured products in navigation
- Complex product catalog
- Fashion, electronics, or large inventory
**Mobile**: Always use drawer with accordion pattern, never megamenu on mobile.
See [megamenu.md](megamenu.md) for megamenu implementation details.
## Key Ecommerce Patterns
### Cart Indicator (CRITICAL)
**Always visible on both desktop and mobile:**
- Desktop: Top-right, cart icon + count badge
- Mobile: Top-right in header (NOT hidden in hamburger drawer)
- This is non-negotiable - users expect cart always accessible
**Badge display:**
- Shows item count (NOT price - confusing when variants change)
- Only visible when cart has items (count > 0)
- Show actual count up to 99, then "99+"
- Position: Top-right corner of cart icon
- ARIA label: `aria-label="Shopping cart with 3 items"`
**Real-time updates:**
- Update count immediately when items added (optimistic UI)
- No page refresh required
- Sync with backend cart state
- Handle errors gracefully (restore count if add fails)
**Click behavior:**
- Option 1: Navigate to cart page
- Option 2: Open cart popup/drawer (see cart-popup.md)
- Choice depends on store type (see cart-popup.md for decision criteria)
**CORRECT:**
- Cart icon visible in mobile header
- Badge shows count (not price)
- Updates in real-time without page refresh
- 44x44px touch target
- Links to cart or opens cart popup
**WRONG:**
- Hiding cart in mobile hamburger drawer (users can't find it)
- Showing price in badge (€25.99) instead of count
- Cart count doesn't update until page refresh
- No visual feedback when items added
### Category Navigation
**CRITICAL: Fetch dynamically from backend (NEVER hardcode):**
**WRONG - These are all incorrect approaches:**
```typescript
// WRONG - Hardcoded array
const categories = ["Women", "Men", "Kids", "Accessories"]
// WRONG - Static object array
const categories = [
{ id: 1, name: "Women", slug: "women" },
{ id: 2, name: "Men", slug: "men" }
]
// WRONG - Importing static data
import { categories } from "./categories.ts"
```
**CORRECT - Fetch from backend API:**
- Medusa: Use SDK category list method (verify exact method with docs/MCP)
- Other backends: Call categories endpoint (check API documentation)
- Fetch on component mount or during server-side rendering
**Why dynamic fetching is mandatory:**
- Store owners add/remove/rename categories frequently
- Category order and hierarchy changes
- Multi-language stores need translated category names
- Featured categories rotate (seasonal, promotions)
- Hardcoded values require developer intervention for simple changes
**Caching strategy:**
- Cache categories (revalidate on interval or manual trigger)
- Use SWR, TanStack Query, or framework-level caching
- Revalidate every 5-10 minutes or on page navigation
- Update immediately when backend categories change
**Organization:**
- 4-7 top-level categories ideal (max 10 on desktop)
- Order comes from backend (respects admin's ordering)
- Keep "Sale" or "New Arrivals" prominent if backend provides it
- Maximum 2 levels in simple dropdown (category → subcategory)
- Deeper hierarchies: Use megamenu or separate category pages
**Desktop behavior:**
- Horizontal links with hover dropdowns for subcategories
- Slight hover delay to prevent accidental triggers
- Click parent to navigate to category page
- Click child to navigate to subcategory
**Mobile behavior:**
- All categories in hamburger drawer
- Accordion pattern for subcategories (expand/collapse)
- Close drawer on category click (except expanding accordion)
- Scrollable drawer if categories exceed viewport height
**CORRECT:**
- Categories fetched from backend API on mount
- Cache with revalidation strategy
- Respects backend ordering and hierarchy
- 4-7 top-level items on desktop (based on what backend returns)
- Accordion for mobile subcategories
- Consistent ordering across devices
**WRONG:**
- Hardcoded category array in component (NEVER DO THIS)
- Static categories imported from file (NEVER DO THIS)
- No cache invalidation (stale categories)
- Too many top-level items (>10, overwhelming)
- Different category order on desktop vs mobile
- Categories don't update when backend changes
### User Account Indicator
**Two states based on authentication:**
**Logged out:**
- Desktop: "Sign In" or "Log In" text + user icon
- Mobile: User icon only
- Click navigates to login page
- Clear call-to-action
**Logged in:**
- Desktop: User name, initials, or email + dropdown
- Mobile: User name/initials or icon → account page
- Dropdown menu (desktop): My Account, Orders, Wishlist, Sign Out
- Fetch current user from backend authentication state
**Authentication state management:**
- Check auth state from backend (not just localStorage)
- Update immediately on login/logout events
- Handle session expiration gracefully
- Sync across tabs if possible
**CORRECT:**
- Shows "Sign In" when logged out
- Shows user identifier when logged in
- Dropdown with account actions
- Checks backend auth state (not just client state)
**WRONG:**
- No indication of login state
- Relies solely on localStorage (can be stale)
- No dropdown for account actions when logged in
- Missing logout option
### Mobile Navigation Pattern
**Hamburger drawer:**
- Trigger: Hamburger icon (top-left)
- Drawer: Slides from left, 80-85% width, full height, scrollable
- Backdrop: Semi-transparent overlay, click to close
- Content: All categories with accordion subcategories
**CRITICAL: Keep cart in header:**
- Cart icon stays in mobile header (top-right)
- Don't hide cart inside drawer
- Users expect cart always accessible
- Same for search icon if using icon-only search
**Account in drawer:**
- Logged out: "Sign In" link in drawer header or top of menu
- Logged in: User name/initials in drawer header with link to account
**Close behavior:**
- Close button (X) in drawer header
- Click backdrop overlay
- Navigate to category (drawer closes)
- Escape key
**CORRECT:**
- Cart stays in mobile header (visible)
- Hamburger opens drawer from left
- Backdrop overlay dims background
- Close on navigation or backdrop click
- Scrollable drawer for long menus
**WRONG:**
- Cart hidden inside hamburger drawer (cardinal sin)
- Full-screen drawer (no backdrop)
- Drawer doesn't close on navigation
- Not scrollable (categories cut off)
### Bottom Navigation (Alternative for Mobile)
**When to use:**
- Store has 3-5 key sections (Home, Browse, Cart, Account, Search)
- App-like experience desired
- Frequent switching between sections
- Not suitable for complex category hierarchies
**Pattern:**
- Fixed bar at bottom of screen (mobile only)
- Icon + label for each section
- Highlight active section
- 5 items maximum
- Direct navigation, no dropdowns
## Layout Structure
**Desktop:**
- Left: Logo → Homepage
- Center: Category links (horizontal)
- Right: Search, Account, Cart
**Mobile:**
- Left: Hamburger
- Center: Logo
- Right: Cart (+ Search icon optional)
**Sticky recommended:**
- Keeps cart/account accessible while scrolling
- Use `position: sticky` or `position: fixed`
- Solid background color (hide scrolling content)
- Adequate z-index to stay above content
## Accessibility Essentials
**Ecommerce-specific ARIA:**
- Cart count: `aria-live="polite"` to announce changes (e.g., "3 items in cart")
- Mobile drawer: `role="dialog"`, `aria-modal="true"`
- Hamburger button: `aria-label="Open navigation menu"`, `aria-expanded="false"`
- Active page: `aria-current="page"` on current category link
- Dropdown indicators: `aria-expanded`, `aria-controls` for megamenu relationships
**Keyboard navigation:**
- Tab through all links/buttons
- Enter/Space to activate
- Escape to close mobile menu or dropdowns
- Visible focus indicators (outline/ring)
**Generic accessibility applies:**
- Semantic HTML (`<header>`, `<nav>`)
- Icon buttons need ARIA labels
- 4.5:1 color contrast minimum
- 44x44px touch targets on mobile
## Common Ecommerce Mistakes
**CRITICAL: Hardcoded static categories** - NEVER create static category arrays like `const categories = ["Women", "Men"]` or import from static files. ALWAYS fetch from backend API. Categories change constantly - new categories added, names changed, ordering updated. Hardcoded categories require developer intervention for simple changes and defeat the purpose of dynamic commerce platforms. This is the #1 most common mistake.
**Hiding cart in mobile drawer** - Users expect cart always visible. Keep cart icon in header (top-right), not hidden inside hamburger menu.
**No real-time cart updates** - Update count immediately when items added (optimistic UI). Don't require page refresh.
**Showing price in cart badge** - Show item count (number), not total price. Price display confuses when variants have different quantities.
**No cache invalidation** - Categories become stale when backend changes. Revalidate periodically (5-10 min) or on manual trigger.
**Hover-only dropdowns on mobile** - Use click/tap interactions. Hover doesn't work on touch devices.
**Desktop navigation on mobile** - Use hamburger drawer pattern, not horizontal menu that doesn't fit.
**Inconsistent category order** - Same order on desktop and mobile for consistency. Respect backend's category ordering.
## Backend Integration
### Category Fetching (CRITICAL - NEVER Hardcode)
**Implementation patterns:**
**Client-side fetching:**
- Fetch categories in useEffect on mount
- Store in state (use appropriate types for Medusa: StoreProductCategory)
- Handle loading and error states
- Map categories to navigation links
- Use category.id as key, category.handle for URL, category.name for display
**With caching (RECOMMENDED):**
- Use TanStack Query with queryKey ['categories']
- Set staleTime: 5-10 minutes (categories rarely change)
- Automatic loading/error states
- Request deduplication if multiple components need categories
**Server-side fetching:**
- Fetch in server component or load function
- No loading state needed (rendered on server)
- Better for SEO
**Cart state synchronization pattern:**
- Subscribe to global cart state (Context)
- Update navbar cart count when cart changes
- Handle optimistic updates (show new count immediately on add to cart)
- Sync with backend on events or interval
**Authentication state pattern:**
- Check auth state from backend on mount
- Listen for login/logout events
- Update account indicator immediately
- Handle session expiration gracefully
**Category update triggers:**
- On page load/navigation
- On manual refresh trigger
- On revalidation interval (5-10 minutes)
- After admin updates categories (webhook or polling)
## Checklist
**Essential navbar features:**
- [ ] **CRITICAL: Categories fetched dynamically from backend API (NOT hardcoded arrays)**
- [ ] **CRITICAL: No static category imports or hardcoded category lists**
- [ ] Desktop: Horizontal category links
- [ ] Mobile: Hamburger drawer with accordion
- [ ] Cart icon visible on both desktop and mobile header (NOT hidden in drawer)
- [ ] Cart badge shows item count (not price)
- [ ] Cart count updates in real-time
- [ ] Categories use backend ordering (not manual ordering)
- [ ] Account indicator shows login state
- [ ] Logo links to homepage
- [ ] 4-7 top-level categories displayed (max 10)
- [ ] Mobile drawer closes on navigation
- [ ] Sticky navigation (recommended)
- [ ] 44x44px minimum touch targets
- [ ] ARIA labels on icon buttons
- [ ] `aria-live` on cart count for screen readers
- [ ] Keyboard accessible with visible focus states
- [ ] Categories cached with revalidation strategy (5-10 min)
- [ ] Error handling for failed category fetch

View File

@@ -0,0 +1,221 @@
# Popups Component
## Contents
- [Overview](#overview)
- [When to Use Popups](#when-to-use-popups)
- [Ecommerce Popup Types](#ecommerce-popup-types)
- [Timing and Triggers](#timing-and-triggers)
- [Frequency Management](#frequency-management)
- [Mobile Considerations](#mobile-considerations)
- [Checklist](#checklist)
## Overview
Popups (modals/overlays) appear over main content to capture attention for specific actions: newsletter signups, promotional offers, exit-intent offers.
**Assumed knowledge**: AI agents know how to build modals with close buttons and backdrop overlays. This focuses on ecommerce popup patterns.
**Critical balance**: Effective for conversions when used sparingly, intrusive and annoying when overused. Timing and frequency are critical for ecommerce.
## When to Use Popups
**Use popups when:**
- Offering significant value (10-20% first-purchase discount, free shipping)
- Time-sensitive promotions (flash sale ending soon)
- Exit-intent to recover abandoning visitors (last chance offer)
- First-time visitor welcome (one-time only)
- Important announcements (shipping delays, policy changes)
**Don't use popups for:**
- Every page visit (extremely annoying)
- Multiple popups per session
- Immediate page load (users haven't seen site yet)
- Mobile users (especially full-screen takeovers - very disruptive)
- Users who already signed up or dismissed
**Consider alternatives:**
- Top banner: Less intrusive, always visible, good for ongoing promotions
- Inline forms: Homepage or footer newsletter signup, non-blocking
- Slide-in (corner): From bottom-right, less disruptive than center popup
- Post-purchase: Ask for email after successful order (high conversion)
**Popups are best when:** Need immediate attention, high-value offer justifies interruption, exit-intent (last chance).
## Ecommerce Popup Types
### 1. First-Purchase Discount
**Purpose**: Convert first-time visitors with discount incentive.
**Content:**
- Headline: "Welcome! Get 10% Off Your First Order"
- Email input
- Discount code or automatic application
- Subscribe button: "Get My Discount"
**Timing**: After 30-60 seconds on site OR after viewing 2-3 products (engagement signal).
**Frequency**: Once per user (cookie/localStorage). Don't show to returning customers.
### 2. Newsletter Signup
**Purpose**: Grow email list for marketing.
**Content:**
- Value proposition: "Get exclusive deals and early access"
- Email input
- Subscribe button
- Optional: First-purchase discount incentive (10-15% off)
**Timing**: After 50% scroll OR 60 seconds on site.
**Frequency**: Once per session. If dismissed, don't show for 30 days.
### 3. Exit-Intent Popup
**Purpose**: Recover abandoning visitors with last-chance offer.
**Trigger**: Mouse moves toward browser close/back button (desktop only).
**Content:**
- Urgency: "Wait! Don't Miss Out"
- Offer: "Take 10% Off Your Order" or "Free Shipping Today Only"
- Email capture (optional): "Send me the code"
- CTA: "Claim Offer" or "Continue Shopping"
**Best for**: Cart abandoners, product page exits, first-time visitors.
**Frequency**: Once per session. Don't show if user already added to cart or on checkout.
### 4. Cart Abandonment Reminder
**Purpose**: Remind user of items in cart before leaving.
**Trigger**: Exit-intent when cart has items but user navigating away.
**Content:**
- "Your Cart is Waiting"
- Show cart summary (items, total)
- CTA: "Complete Your Order" or "View Cart"
- Optional incentive: "Complete in 10 minutes for free shipping"
**Frequency**: Once per session with items in cart.
### 5. Promotional Announcement
**Purpose**: Announce sales, new arrivals, or site-wide events.
**Content:**
- Headline: "Flash Sale: 40% Off Everything"
- Subtext: "Ends in 3 hours"
- CTA: "Shop Now"
**Timing**: Immediate on page load (if major event), OR after 30 seconds.
**Frequency**: Once per day during promotion period.
## Timing and Triggers
**Time-based:**
- 30-60 seconds after page load (enough time to browse)
- Never immediate (0 seconds) - users need to see site first
**Engagement-based:**
- After 50% scroll (shows interest)
- After viewing 2-3 products (qualified visitor)
- After adding to cart (exit-intent only)
**Exit-intent:**
- Mouse moves toward close/back button (desktop)
- Scroll-up toward address bar (mobile - less reliable)
- Only trigger once per session
- Don't trigger on checkout pages (interrupts purchase)
**Page-specific:**
- Homepage: Welcome/discount popup
- Product pages: Exit-intent with product-specific offer
- Cart page: Don't use popups (already engaged)
- Checkout: Never use popups (critical flow)
## Frequency Management
**Critical for UX**: Don't show same popup repeatedly to same user.
**Implementation:**
1. **Cookie/localStorage tracking**: Store dismissal/signup with timestamp
2. **Respect dismissals**: If user closes popup, don't show for 30 days
3. **Signed-up users**: Never show newsletter popup again
4. **Session limits**: Max 1 popup per session
5. **Time cooldown**: If dismissed, wait 30 days before showing again
**Example tracking:**
```javascript
// On popup dismiss
localStorage.setItem('popup_dismissed', Date.now())
localStorage.setItem('popup_type', 'welcome_discount')
// Before showing popup
const dismissedTime = localStorage.getItem('popup_dismissed')
const daysSince = (Date.now() - dismissedTime) / (1000 * 60 * 60 * 24)
if (daysSince < 30) {
// Don't show popup
}
```
**Progressive disclosure:**
- Session 1: Welcome discount popup
- Session 2+: Exit-intent only (if applicable)
- Never stack multiple popups
## Mobile Considerations
**Mobile popups are MORE intrusive:**
- Smaller screen = popup takes more space
- Harder to close (small X button)
- Disrupts mobile browsing flow
- Can hurt mobile SEO (Google penalty for intrusive interstitials)
**Mobile best practices:**
1. **Use sparingly**: Consider top banner or inline forms instead
2. **Make easily dismissable**: Large close button (44x44px), tap outside to close
3. **Delay longer**: 60+ seconds instead of 30 seconds
4. **Smaller size**: 90% width max, not full-screen
5. **Exit-intent**: Less reliable on mobile, avoid
6. **Google penalty**: Avoid full-screen popups on mobile (hurts rankings)
**Mobile alternative**: Sticky bottom bar (less intrusive)
- "Get 10% Off - Sign Up" with email input
- Always visible but doesn't block content
- Better mobile UX than popup
## Checklist
**Essential features:**
- [ ] Clear value proposition (discount, benefit)
- [ ] Single focused CTA
- [ ] Easy to close (X button, backdrop click, Escape key)
- [ ] Delayed timing (30-60s, not immediate)
- [ ] Frequency management (localStorage/cookie tracking)
- [ ] Respect dismissals (30-day cooldown)
- [ ] Never show to signed-up users
- [ ] Max 1 popup per session
- [ ] Exit-intent for cart abandoners (desktop only)
- [ ] Don't show on checkout pages
- [ ] Mobile: Use sparingly, consider alternatives
- [ ] Mobile: Large close button (44x44px)
- [ ] Mobile: Not full-screen (90% width max)
- [ ] Email validation before submit
- [ ] Loading state on submit
- [ ] Success message or redirect
- [ ] Keyboard accessible (Tab, Escape, Enter)
- [ ] `role="dialog"` and `aria-modal="true"`
- [ ] Focus trap (keep focus within popup)
- [ ] ARIA label on close button
- [ ] Screen reader announcements on open

View File

@@ -0,0 +1,125 @@
# Product Card Component
## Contents
- [Overview](#overview)
- [Price Display (Ecommerce-Specific)](#price-display-ecommerce-specific)
- [Action Buttons and Variant Handling](#action-buttons-and-variant-handling)
- [Badges and Labels](#badges-and-labels)
- [Mobile Considerations](#mobile-considerations)
- [Ecommerce Checklist](#ecommerce-checklist)
## Overview
Product cards display products in grids (product listings, search results, related products). Key ecommerce considerations: clear pricing, quick add-to-cart, and stock indicators.
**Assumed knowledge**: AI agents know how to build cards with images, titles, and buttons. This guide focuses on ecommerce-specific patterns.
### Key Ecommerce Requirements
- Clear, prominent pricing (including sale prices)
- Variant handling for add-to-cart
- Stock status indicators
- Sale/New/Out of Stock badges
- Responsive grid (1 col mobile, 2-3 tablet, 3-4 desktop)
- Fast image loading (lazy load, optimized)
## Price Display (Ecommerce-Specific)
### Regular vs Sale Pricing
**Sale price display:**
- Sale price: Larger, bold, red or accent color
- Original price: Smaller, struck through (~~$79.99~~), gray
- Position sale price before original price
- Optional: Show discount percentage badge (-20%)
**Format consistently:**
- Always include currency symbol ($49.99)
- Consistent decimals ($49.99 not $49.9 or $50)
- For Medusa: Display prices as-is (no divide by 100)
### Price Range (Multiple Variants)
**When variants have different prices:**
- Show "From $49" or "$49 - $79"
- Makes it clear price varies by selection
- Don't show range if all variants same price
## Action Buttons and Variant Handling
### Add to Cart with Variants (CRITICAL)
**Key challenge**: Products with variants require variant selection before adding to cart.
**Handling strategies:**
1. **Add first variant by default** - Click adds `product.variants[0]`. Fast for simple products (1-2 variants).
2. **Redirect to product page** - Navigate to detail page for variant selection. Best for complex products (size + color + material).
3. **Quick View modal** - Variant selector in modal. Good middle ground (desktop only).
**Decision:**
- Simple products (1-2 variants): Add first variant
- Fashion/apparel with sizes: Require size selection (redirect or Quick View)
- Complex products (3+ variant types): Redirect to product page
**Button behavior:**
- Loading state ("Adding..."), disable during loading
- Optimistic UI update (cart count immediately)
- Success feedback (toast, cart popup, or checkmark)
- **Don't navigate away** (stay on listing page)
- Handle errors (out of stock, API failure)
**Wishlist button (optional)**: Heart icon, top-right over image. Empty when not saved, filled (red) when saved. Refer to wishlist.md for more details.
## Badges and Labels
**Badge priority** (show max 1-2 per card):
1. **Out of Stock** (highest) - Gray/black overlay on image, disables add-to-cart
2. **Sale/Discount** - "Sale" or "-20%", red/accent, top-left corner
3. **New** - "New" for recent products, blue/green, top-left corner
4. **Low Stock** (optional) - "Only 3 left", orange, creates urgency
**Display**: Top-left corner (except Out of Stock overlay), small but readable, high contrast.
## Mobile Considerations
### Grid Layout
**Mobile-specific adjustments:**
- 2 columns maximum on mobile (never 3+)
- Larger touch targets (44px minimum for buttons)
- Always show "Add to Cart" button (no hover-only)
- Simplified content (hide optional elements like brand)
- Smaller images for performance (<400px wide)
### Touch Interactions
**No hover states on mobile:**
- Don't hide actions behind hover
- Always show primary button
- Use tap states (active state) instead of hover
## Ecommerce Checklist
**Essential features:**
- [ ] Clear product image (optimized, lazy loaded)
- [ ] Product title (truncated to 2 lines max)
- [ ] Price prominently displayed
- [ ] Sale price shown correctly (struck-through original price)
- [ ] Currency symbol included
- [ ] For Medusa: Price displayed as-is (not divided by 100)
- [ ] Add to Cart button with loading state
- [ ] Variant handling strategy (first variant, redirect, or Quick View)
- [ ] Optimistic UI update (cart count immediately)
- [ ] Success feedback (toast or cart popup)
- [ ] Don't navigate away after adding to cart
- [ ] Out of Stock badge (disables add-to-cart)
- [ ] Sale badge when price reduced
- [ ] Responsive grid (1 col mobile, 2-3 tablet, 3-4 desktop)
- [ ] Touch-friendly on mobile (44px buttons)
- [ ] Keyboard accessible (focus states, Enter to activate)
- [ ] Descriptive alt text on images
- [ ] Semantic HTML (`<article>` wrapper)

View File

@@ -0,0 +1,217 @@
# Product Reviews Component
## Contents
- [Overview](#overview)
- [Review Display Patterns](#review-display-patterns)
- [Rating Summary and Distribution](#rating-summary-and-distribution)
- [Sorting and Filtering](#sorting-and-filtering)
- [Review Submission](#review-submission)
- [Trust Signals](#trust-signals)
- [SEO Integration](#seo-integration)
## Overview
Product reviews build trust and influence purchase decisions. Reviews with ratings convert 270% better than products without.
**Assumed knowledge**: Claude knows how to build forms and display lists. This guide focuses on ecommerce review patterns and trust signals.
### Key Requirements
- Star rating summary (1-5 stars) with distribution
- Individual reviews with ratings, text, author, date
- Sorting (Most Recent, Most Helpful, Highest/Lowest Rating)
- Filtering by rating (5 stars only, 4+ stars)
- Verified purchase badges
- Helpful votes (upvote system)
- Review submission form
- Mobile-optimized
## Review Display Patterns
### Placement
**On product page:**
- Below product details (after add-to-cart)
- Before related products
- Anchor link in product info: "★★★★★ (127 reviews)"
**Separate reviews page:**
- Only for very large catalogs (500+ reviews)
- Link: "View All Reviews"
- Most stores show reviews inline on product page
## Rating Summary and Distribution
### Average Rating Display
**Show prominently:**
- Average rating: "★★★★★ 4.5 out of 5"
- Total review count: "Based on 127 reviews"
- Large stars (24-32px)
### Rating Distribution (CRITICAL)
**Visual breakdown with clickable bars:**
```
5 ★ [████████████████████] 82 (65%)
4 ★ [██████░░░░░░░░░░░░░░] 25 (20%)
3 ★ [██░░░░░░░░░░░░░░░░░░] 10 (8%)
2 ★ [█░░░░░░░░░░░░░░░░░░░] 5 (4%)
1 ★ [█░░░░░░░░░░░░░░░░░░░] 5 (3%)
```
**Make bars clickable:**
- Click to filter reviews by rating
- Shows only selected star ratings
- "Show all" to reset filter
**Why distribution matters:**
- Perfect 5.0 rating seems fake (customers trust 4.2-4.5 average)
- Showing negative reviews builds trust
- Distribution helps customers understand product quality
### No Reviews State
**When no reviews:**
- "No reviews yet"
- "Be the first to review this product"
- "Write a Review" button prominent
- Don't show 0 stars or empty rating
## Sorting and Filtering
### Sort Options (CRITICAL)
**Essential sorting:**
- **Most Recent** (default) - shows latest feedback
- **Most Helpful** (by upvotes) - surfaces best reviews
- **Highest Rating** (5 stars first) - see positive feedback
- **Lowest Rating** (1 star first) - see concerns
**Dropdown selector:**
```
Sort by: [Most Recent ▾]
```
### Filter Options
**Filter by rating:**
- All ratings (default)
- 5 stars only
- 4+ stars
- 3 stars or less (see negative feedback)
**Filter by criteria:**
- Verified purchases only (highest trust)
- With photos only (visual proof)
- Recent (last 30 days, 6 months)
**Show filtered count:**
- "Showing 24 of 127 reviews"
## Review Submission
### Review Form Fields
**Required:**
- Star rating (1-5 stars selector)
- Review text (textarea, 50-500 characters)
- Reviewer name (if not logged in)
**Optional:**
- Review title/headline
- Upload images (2-5 max)
- Would you recommend? (Yes/No)
**Form placement:**
- "Write a Review" button opens modal or inline form
- Position near rating summary
### Form Validation
**Requirements:**
- Rating must be selected
- Minimum review length (50 characters)
- Show character counter: "50 / 500 characters"
- Validate before submit
**Success:**
- "Thank you for your review!"
- "Your review is pending approval" (if moderation enabled)
## Trust Signals
### Verified Purchase Badge (CRITICAL)
**Display:**
- Badge or checkmark: "✓ Verified Purchase"
- Position near reviewer name
- Green color or checkmark icon
- Only for confirmed customers
**Why it matters:**
- Builds trust (real customer, not fake)
- Reduces suspicion of paid reviews
- Higher credibility
### Helpful Votes
**Upvote/downvote system:**
- "Was this review helpful?"
- [👍 Yes (24)] [👎 No (2)]
- Click to vote (one vote per user)
- Powers "Most Helpful" sorting
**Benefits:**
- Surfaces most useful reviews
- Community validation
- Reduces impact of unhelpful reviews
### Review Images (Optional)
Customer-uploaded photos (3-4 max per review, 60-80px thumbnails, click to enlarge). Visual proof increases trust and engagement.
### Store Responses (Recommended)
Seller replies below original review (indented, light gray background). Respond to negative reviews professionally - shows you care, addresses concerns without being defensive.
## SEO Integration
**AggregateRating Schema (CRITICAL):** Add structured data to show star ratings in search results. Include `ratingValue` (avg rating), `reviewCount`, `bestRating` (5), `worstRating` (1).
**SEO benefits:** Star ratings in search results, higher CTR, rich snippets. See seo.md for implementation details.
**Important:** Only include if reviews are real. Fake reviews violate Google guidelines.
## Display Patterns
**Individual review card:**
Star rating (16-20px) + text + reviewer name (first name + initial) + date + verified badge + helpful votes. Truncate long reviews (200-300 chars) with "Read more".
**Mobile:**
Single column, touch-friendly votes (44px), full-screen sort select, filter bottom sheet, "Load more" pagination.
## Checklist
**Essential features:**
- [ ] Star rating summary (average + count)
- [ ] Rating distribution bar chart (5 to 1 stars)
- [ ] Clickable bars to filter by rating
- [ ] Sort dropdown (Most Recent, Most Helpful, Highest/Lowest)
- [ ] Filter options (verified, with photos, by rating)
- [ ] Individual reviews with: stars, text, name, date
- [ ] Verified purchase badge
- [ ] Helpful votes (upvote/downvote)
- [ ] Review submission form (rating, text)
- [ ] Form validation (minimum length, required rating)
- [ ] "Read more" for long reviews
- [ ] Store responses to reviews (recommended)
- [ ] Review images (customer uploads, optional)
- [ ] Mobile: Touch targets 44px minimum
- [ ] Pagination or "Load more" button
- [ ] No reviews state ("Be the first to review")
- [ ] AggregateRating structured data (SEO)
- [ ] ARIA labels for star ratings
- [ ] Keyboard accessible (all interactions)

View File

@@ -0,0 +1,174 @@
# Product Slider Component
## Contents
- [Overview](#overview)
- [When to Use Product Sliders](#when-to-use-product-sliders)
- [Slider Patterns](#slider-patterns)
- [Product Display](#product-display)
- [Navigation Controls](#navigation-controls)
- [Mobile Sliders](#mobile-sliders)
- [Performance](#performance)
- [Checklist](#checklist)
## Overview
Product slider (carousel) displays multiple products horizontally with navigation to scroll through them. Used for related products, recently viewed, bestsellers, and featured products.
**Assumed knowledge**: AI agents know how to build carousels with navigation. This focuses on ecommerce product slider patterns.
**Key requirements:**
- Horizontal scrolling of product cards
- Arrow navigation (prev/next)
- Optional dot indicators
- Mobile: Swipe gesture support
- Responsive product count (4-6 visible desktop, 2-3 mobile)
- Lazy loading for off-screen products
## When to Use Product Sliders
**Use for:**
- Related products (product page)
- Recently viewed (product page, homepage)
- "You May Also Like" (product page)
- Bestsellers / Featured products (homepage)
- "Frequently Bought Together" (product page)
- New arrivals (homepage)
- Category showcases (homepage)
**Don't use for:**
- Main product images (use gallery instead)
- Critical content (not all users scroll/swipe)
- Checkout flow (keep linear)
- Primary navigation (use grid for discoverability)
## Slider Patterns
**Continuous scroll:**
- Shows 4-6 products at once (desktop)
- Scroll left/right by 1-2 products at a time
- Smooth animated transition (300-400ms)
- Most common pattern
**Infinite loop (optional):**
- Wraps to beginning after end
- Good for small product sets (<10 items)
- Creates continuous browsing feel
- Not necessary for large sets
**Snap to alignment:**
- Products snap to grid after scroll
- Prevents partial product visibility
- Better visual alignment
- Improves browsing experience
**Auto-play (NOT recommended for products):**
- Automatic scrolling without user action
- Poor UX for product sliders (users lose control)
- Only use for promotional banners/hero images
- If using: Pause on hover, slow speed (5-7s)
## Product Display
**Product cards in sliders:**
- Same cards as product grids (see product-card.md)
- Simplified on mobile (less detail, smaller images)
- Image, title, price minimum
- Optional: Rating, "Add to Cart" (desktop only)
- Adequate spacing between cards (16-24px)
**Responsive display:**
- Large desktop (>1440px): 5-6 products visible
- Desktop (1024-1440px): 4-5 products
- Tablet (768-1024px): 3-4 products
- Mobile (<768px): 2 products (sometimes 1.5 for scroll hint)
**Scroll hint on mobile:**
- Show 1.5 products (partial visibility of next)
- Indicates more content to swipe
- Improves discoverability
- Better than showing exact 2 products
## Navigation Controls
**Arrow buttons:**
- Left/right arrows outside slider
- Desktop: Always visible or show on hover
- Mobile: Hidden (swipe gesture preferred)
- Position: Vertically centered
- Size: 40-48px touch targets
- Disable left arrow at start, right arrow at end (no infinite loop)
**Dot indicators (optional):**
- Show progress through slider
- Each dot = one "page" of products
- Position: Below slider, centered
- Small (8-12px dots)
- Only if many products (>12)
- Less common for product sliders (more for hero carousels)
**Keyboard navigation:**
- Tab through visible product cards
- Arrow keys scroll slider (optional)
- Focus management on scroll
## Mobile Sliders
**Touch gestures:**
- Horizontal swipe to scroll
- Native scroll momentum
- Snap to product alignment
- No arrow buttons (swipe is intuitive)
**Mobile-specific adjustments:**
- 2 products visible (or 1.5 for hint)
- Larger touch targets on products
- Remove hover-only features (Quick View)
- Faster scroll animations (200-300ms)
**Performance on mobile:**
- Lazy load off-screen products
- Smaller image sizes
- Limit initial products loaded (8-10)
- Load more on scroll
## Performance
**Lazy loading (critical):**
- Only load visible products initially
- Load adjacent products (left/right) on demand
- Significantly improves page load time
- Use Intersection Observer API
**Image optimization:**
- Responsive images (smaller for mobile)
- WebP format with fallback
- Lazy load off-screen images
- Optimized thumbnails (<300KB)
**Limit slider length:**
- Max 20-30 products per slider
- "View All" link to full category page
- Improves performance
- Prevents endless scrolling
## Checklist
**Essential features:**
- [ ] 4-6 products visible (desktop), 2 (mobile)
- [ ] Arrow navigation (desktop)
- [ ] Swipe gesture (mobile)
- [ ] Product cards with image, title, price
- [ ] Responsive product count
- [ ] Smooth scroll transitions (300-400ms)
- [ ] Snap to product alignment
- [ ] Lazy load off-screen products
- [ ] "View All" link if many products (>20)
- [ ] Disable arrows at start/end
- [ ] Keyboard accessible (Tab through products)
- [ ] Mobile: No arrows, swipe only
- [ ] Optimized images (<300KB)
- [ ] Spacing between products (16-24px)
- [ ] ARIA labels on navigation (`aria-label="Previous products"`)
- [ ] `role="region"` on slider container
- [ ] NO auto-play for product sliders

View File

@@ -0,0 +1,101 @@
# Search Component
## Contents
- [Overview](#overview)
- [Search Placement](#search-placement)
- [Autocomplete and Product Suggestions](#autocomplete-and-product-suggestions)
- [Search Results Page](#search-results-page)
- [Empty States](#empty-states)
- [Recent and Popular Searches](#recent-and-popular-searches)
- [Mobile Search](#mobile-search)
## Overview
Search is critical for ecommerce - users with search intent convert at higher rates. Provide fast, relevant product discovery with autocomplete.
**Assumed knowledge**: AI agents know how to build search inputs with icons and clear buttons. This guide focuses on ecommerce search patterns.
### Key Requirements
- Prominent search input (always accessible)
- Instant autocomplete after 2-3 characters
- Product suggestions with images
- Fast, relevant search results
- Filters to refine results
- Empty state guidance
- Mobile full-screen search modal
## Search Placement
**Desktop**: Navbar center (between logo and cart) or right side. Always visible, 300-500px width. Part of sticky navbar. Never hide in hamburger menu.
**Mobile**: Magnifying glass icon in top-right (44x44px minimum). Opens full-screen modal - eliminates distractions, maximizes suggestion space, better typing experience.
## Autocomplete and Product Suggestions
**Show suggestions** after 2-3 characters (not 1). Debounce 300ms to prevent excessive API calls.
**Display 5-10 product suggestions:**
- Small image (40-60px), title, price
- Clickable to product page
- Optional: Category/brand suggestions, popular terms
- Divide sections with headers
- "View all results for [query]" footer link
**Backend integration**: Fetch from search API. Check with ecommerce platform's documentation for API reference.
## Search Results Page
**Header**: "Search Results for '[query]'" + result count ("24 products found"). Search bar visible and pre-filled for refining.
**Grid layout**: Same as product listings (see product-listing.md). 1-4 columns based on device.
**Sorting**: Relevance (default, unique to search), Price Low/High, Newest.
**Filters**: Sidebar (desktop) or drawer (mobile). Category, Price, Brand, Availability with result counts.
## Empty States
**No results**: "No results for '[query]'" with helpful suggestions (check spelling, try broader keywords, browse categories). "Browse All Products" button + links to popular categories.
**Loading state**: Product card skeletons (6-8 cards), minimum 300ms display to avoid flashing.
## Recent and Popular Searches
**Recent searches** (user-specific, localStorage): Show 3-5 recent searches when input focused (before typing). Helps re-search without retyping.
**Popular searches** (site-wide, from backend): Show 5-10 trending terms when focused. Pill/tag styling.
Display both on: Empty input focus (desktop dropdown), mobile modal open.
## Mobile Search
**Full-screen modal pattern:**
- Header: Back button (44x44px) + search input (48px height, auto-focus, `type="search"`)
- Content: Recent/popular searches (empty), autocomplete (typing), scrollable
- Close: Back button, device back gesture, Escape key
## Ecommerce Search Checklist
**Essential features:**
- [ ] Prominent search input in navbar (desktop)
- [ ] Search icon clearly visible (mobile)
- [ ] Full-screen modal on mobile tap
- [ ] Autocomplete after 2-3 characters
- [ ] Debounced API calls (300ms)
- [ ] Product suggestions with images, prices
- [ ] "View all results" link in dropdown
- [ ] Search results page shows query
- [ ] Result count displayed
- [ ] Sort by Relevance (default for search)
- [ ] Filters for refining results (category, price, brand)
- [ ] Empty state with helpful guidance
- [ ] Loading state indicator (skeleton)
- [ ] Recent searches (localStorage)
- [ ] Popular searches (from backend)
- [ ] Mobile: Auto-focus, large input (48px)
- [ ] Keyboard navigation (arrow keys, Enter, Escape)
- [ ] ARIA labels (`role="search"`, `aria-label`)
- [ ] Accessible to screen readers

View File

@@ -0,0 +1,391 @@
# Connecting to Backend
## Contents
- [Overview](#overview)
- [Detecting the Backend](#detecting-the-backend-critical)
- [Framework Detection](#framework-detection)
- [Environment Configuration](#environment-configuration)
- [Backend-Specific Integration](#backend-specific-integration)
- [Authentication Patterns](#authentication-patterns)
- [Cart State Management](#cart-state-management)
- [Error Handling for Ecommerce](#error-handling-for-ecommerce)
- [Performance Patterns](#performance-patterns)
- [Data Fetching with TanStack Query](#data-fetching-with-tanstack-query-recommended)
- [Checklist](#checklist)
## Overview
Best practices for connecting storefront to ecommerce backend APIs. Framework-agnostic patterns for authentication, cart state management, error handling, and performance optimization.
**For Medusa-specific integration**, see `reference/medusa.md` for SDK setup, pricing, regions, and Medusa patterns.
## Detecting the Backend (CRITICAL)
**Before implementing any backend integration, identify which ecommerce backend is being used.**
### Detection Strategy
**1. Check for monorepo structure:**
```bash
# Look for backend directory
ls -la ../backend
ls -la ./backend
ls -la ../../apps/backend
```
Common monorepo patterns:
- `/apps/storefront` + `/apps/backend`
- `/frontend` + `/backend`
- `/packages/web` + `/packages/api`
**2. Check package.json dependencies:**
```json
{
"dependencies": {
"@medusajs/js-sdk": "...", // Medusa
// check other ecommerce frameworks...
}
}
```
**3. Check environment variables:**
```bash
# Look in .env, .env.local, .env.example
grep -i "api\|backend\|medusa\|shopify\|commerce" .env*
```
Common patterns:
- `NEXT_PUBLIC_MEDUSA_BACKEND_URL` → Medusa
- Custom `API_URL` or `BACKEND_URL` → Other backend
**4. If unsure, ASK THE USER:**
```markdown
I need to connect to the ecommerce backend. Which backend are you using?
Options:
- Medusa (open-source headless commerce)
- Custom backend
- Other
```
### Backend Documentation and MCP Servers
**ALWAYS refer to the backend's official documentation or MCP server for:**
- API endpoints and data structures
- Authentication requirements
- SDK usage and installation
- Environment configuration
- Rate limits and best practices
**For Medusa:**
- Documentation: https://docs.medusajs.com
- MCP Server: If available, use Medusa MCP server for real-time API information
- JS SDK docs: https://docs.medusajs.com/resources/js-sdk
- See `reference/medusa.md` for detailed integration guide
**For other backends:**
- Check the backend's documentation portal
- Look for MCP server if available
- Verify API endpoints and authentication methods
- Never assume API structure without verification
**Important:** Do not guess API endpoints or data formats. Always verify with documentation or ask the user to confirm the backend's API structure.
## Framework Detection
Identify the frontend framework to determine appropriate data fetching patterns:
**Next.js:**
- App Router: Server Components (async/await), Client Components (useEffect/TanStack Query)
- Pages Router: getServerSideProps/getStaticProps (server), useEffect (client)
**SvelteKit:**
- Load functions for server-side data
- Client-side: fetch in component lifecycle
**TanStack Start:**
- Server functions for server-side data
- Client-side: fetch with React hooks
**General Rule:**
- **Server-side for initial load**: SEO, performance, security (product pages, listings)
- **Client-side for interactions**: Cart, filters, search, user-specific data
## Environment Configuration
**Store API URLs and keys in environment variables:**
```typescript
// .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_PUBLISHABLE_KEY=pk_...
```
**Framework-specific prefixes:**
- Next.js: `NEXT_PUBLIC_` for client-side
- SvelteKit: `PUBLIC_` for client-side
- Vite-based (TanStack Start): `VITE_` for client-side
**Security:**
- ❌ NEVER expose secret/admin keys in client-side code
- ✅ Publishable keys are safe for client (Medusa, Stripe)
- ✅ Secret keys only in server-side code or environment
## Backend-Specific Integration
### Medusa Backend
**For complete Medusa integration guide**, see `reference/medusa.md` which covers:
- SDK installation and setup
- Vite configuration (for TanStack Start, etc.)
- TypeScript types from `@medusajs/types`
- Price display (never divide by 100)
- Common operations (products, cart, categories, customers)
- Custom endpoints
- Region state management
- Error handling with SDK
### Other Backends
For non-Medusa backends (custom APIs, third-party platforms):
**1. Consult backend's API documentation** for:
- Authentication requirements
- Available endpoints
- Request/response formats
- SDK availability (check if official SDK exists)
**2. Use backend's official SDK if available** - provides type safety, error handling, and best practices
**3. If no SDK, create API client wrapper:**
- Centralize API calls in one module
- Group by resource (products, cart, customers, orders)
- Handle authentication (include tokens/cookies)
- Handle errors consistently
- Use native fetch or axios
## Authentication Patterns
### Customer Authentication
**Session-based (cookies):**
- Backend manages session via cookies
- No manual token management needed
- Works across page refreshes
- Common in traditional ecommerce backends
- Call backend login endpoint, check auth state, logout methods
**Token-based (JWT, OAuth):**
- Store token in localStorage or secure cookie after login
- Include token in Authorization header for all authenticated requests
- Common in headless/API-first backends
- Format: `Authorization: Bearer {token}`
### Protecting Customer Routes
**Check authentication before rendering customer-specific pages** (account, orders, addresses):
- **Server-side**: Check auth in server functions (getServerSideProps, load functions, etc.). Redirect to login if not authenticated.
- **Client-side**: Check auth state on mount. Redirect to login if not authenticated.
Use framework-specific auth patterns for redirects.
### Cart Access Pattern
**Guest carts:**
- Store cart ID in localStorage or cookie
- Check for existing cart ID on app load
- Create new cart if none exists
- Allows shopping without account
- Persists across sessions
**Logged-in carts:**
- Associate cart with customer account
- Syncs across devices
- **CRITICAL: Merge guest cart with customer cart on login** - Transfer guest cart items to customer's account cart, then clear guest cart ID from localStorage
## Cart State Management
**Critical ecommerce pattern**: Cart must be accessible throughout the app.
### Global Cart State
**React Context (for simple cases):**
- Create CartContext and CartProvider
- Store cart state and cartId (from localStorage)
- Load cart on mount if cartId exists
- Provide methods: addItem, removeItem, updateQuantity, clearCart
- Update cart state after each operation
**State management libraries (Zustand, Redux):**
- Use for complex state requirements
- Better for large applications
- Easier to debug with DevTools
- Same pattern: Store cart, provide actions, sync with backend
**Key requirements:**
- Cart accessible from any component
- Real-time cart count updates
- Optimistic UI updates (update UI immediately, sync with backend)
### Cart Cleanup After Order Placement (CRITICAL)
**IMPORTANT: After order is successfully placed, you MUST reset the cart state.**
**Common issue:** Cart popup and global cart state still show old items after order completion. This happens when cart state isn't cleared after checkout.
**Required cleanup actions:**
1. **Clear cart from global state** - Reset cart state to null/empty in Context/Zustand/Redux
2. **Clear localStorage cart ID** - Remove cart ID: `localStorage.removeItem('cart_id')`
3. **Invalidate cart queries** - If using TanStack Query: `queryClient.invalidateQueries({ queryKey: ['cart'] })`
4. **Update cart count to 0** - Navbar and UI should reflect empty cart
**When to clear:**
- After successful order placement (order confirmed)
- On navigation to order confirmation page
- Before redirecting to thank you page
**Why this is critical:**
- Prevents "phantom cart" from appearing in cart popup after order
- Ensures clean state for next shopping session
- Improves UX by not showing old cart items
## Error Handling for Ecommerce
### Ecommerce-Specific Errors
**Out of stock:**
- Catch errors when adding to cart
- Check for "out of stock" or "inventory" in error message
- Show user-friendly message: "Sorry, this item is now out of stock"
- Update product availability UI to show out of stock
**Price changed during checkout:**
- Compare cart total with expected total
- If different, show warning: "Prices have been updated. Please review your cart."
- Highlight changed prices in cart
**Payment failed:**
- Catch errors during order completion
- Check for specific payment errors: payment_declined, insufficient_funds, etc.
- Show specific messages:
- Payment declined → "Payment declined. Please try a different payment method."
- Insufficient funds → "Insufficient funds. Please use a different card."
- Generic → "Payment failed. Please try again or contact support."
**Session expired:**
- Catch 401/Unauthorized errors
- Clear auth state
- Redirect to login with message: "Your session has expired. Please log in again."
### User-Friendly Error Messages
**Transform technical errors to clear messages:**
- Network/fetch errors → "Unable to connect. Please check your internet connection."
- Timeout errors → "Request timed out. Please try again."
- Inventory errors → "This item is no longer available in the requested quantity."
- Generic fallback → "Something went wrong. Please try again or contact support."
**Pattern**: Check error message or status code, map to user-friendly message, show in UI (toast, banner, inline).
## Performance Patterns
### Data Fetching with TanStack Query (RECOMMENDED)
**Use TanStack Query for all backend API calls** - provides automatic caching, request deduplication, loading/error states, and optimistic updates.
**Installation:** `npm install @tanstack/react-query`
**Setup:**
- Create QueryClient with default options (staleTime: 5 min, retry: 1)
- Wrap app with QueryClientProvider
**Query pattern (for fetching data):**
- Use `useQuery` with queryKey and queryFn
- queryKey: Array with resource and identifier `['products', categoryId]`
- queryFn: API call function
- Returns: `data`, `isLoading`, `error`
- Use for: Products, cart, customer data, categories
**Mutation pattern (for modifying data):**
- Use `useMutation` with mutationFn
- mutationFn: API operation (add to cart, update, delete)
- onSuccess: Update cache or invalidate queries
- Returns: `mutate` function, `isPending` state
- Use for: Add to cart, remove from cart, update quantities, place order
**Benefits:**
- Automatic caching (no manual cache management)
- Built-in loading/error states
- Request deduplication
- Optimistic updates (update UI before server responds)
- Cache invalidation strategies
**Ecommerce-specific usage:**
- Products: Long stale time (5-10 min) - products don't change often
- Cart: Short or no stale time - prices/inventory can change
- Categories: Long stale time - rarely change
### Caching Strategy
**Client-side caching:**
- TanStack Query handles automatically with `staleTime` and `cacheTime`
- Configure globally or per-query
- Product data: 5-10 min stale time
- Cart data: Fresh on every fetch
- Categories: Long stale time
**Server-side caching (framework-specific):**
- Next.js: Use `revalidate` export or cache configuration
- Set revalidation period (e.g., 300 seconds for product pages)
- Static generation with ISR for product pages
### Request Deduplication
TanStack Query and modern frameworks handle this automatically - multiple components requesting same data result in single request.
### Pagination Pattern
**Offset-based:** Pass limit and offset parameters to API `limit: 24, offset: page * 24`
**Cursor-based (better performance):** Pass limit and cursor (last item ID) `limit: 24, cursor: lastProductId`
Check backend documentation for supported pagination type.
## Checklist
**Essential backend integration:**
- [ ] Backend detected (Medusa, Shopify, custom, etc.)
- [ ] Environment variables configured (API URL, keys)
- [ ] Framework-specific data fetching patterns identified
- [ ] **RECOMMENDED: TanStack Query installed and configured for API calls**
- [ ] Server-side fetching for product pages (SEO)
- [ ] Client-side fetching for cart and user interactions (use TanStack Query)
- [ ] Authentication flow implemented (login/logout)
- [ ] Cart ID persisted in localStorage or cookies
- [ ] Global cart state management (context or store)
- [ ] Cart count synced across app
- [ ] Optimistic UI updates for cart operations
- [ ] Error handling for out of stock scenarios
- [ ] Error handling for payment failures
- [ ] Session expiration handling (redirect to login)
- [ ] User-friendly error messages (not technical)
- [ ] Caching strategy for product data
- [ ] Stock availability checks before checkout
- [ ] Price change detection and warnings
**For Medusa backends, also check:**
- [ ] Medusa SDK installed (`@medusajs/js-sdk` + `@medusajs/types`)
- [ ] SDK initialized with baseUrl and publishableKey
- [ ] Vite SSR config added (if using TanStack Start/Vite)
- [ ] Using official types from `@medusajs/types`
- [ ] Not dividing prices by 100 (display as-is)
- [ ] Region context implemented for multi-region stores
- [ ] Region passed to cart and product queries
See `reference/medusa.md` for complete Medusa integration guide.

View File

@@ -0,0 +1,388 @@
# Design Guidelines
## Contents
- [Overview](#overview)
- [Discovering Existing Brand Identity](#discovering-existing-brand-identity)
- [Critical Consistency Rules](#critical-consistency-rules)
- [When to Ask User Approval](#when-to-ask-user-approval)
- [New Project Setup](#new-project-setup)
- [Decision Tree](#decision-tree)
- [Common Mistakes](#common-mistakes)
## Overview
**Purpose:** Provide guardrails to maintain brand consistency when building UI components. This prevents agents from accidentally introducing inconsistent colors, fonts, or design patterns.
**Critical principle:** ALWAYS discover and use existing design tokens before creating new components. NEVER introduce new colors or fonts without user approval.
**When to apply:** Before creating any UI component or design-related change.
## Discovering Existing Brand Identity
Before implementing any component, identify existing brand colors, typography, and design patterns. AI agents can do this - focus on WHAT to look for, not detailed HOW.
### What to Look For
**Colors:**
1. **Tailwind config** (`tailwind.config.ts/js`) - Check `theme.extend.colors` or `theme.colors`
2. **CSS variables** (globals.css, app.css) - Look for `:root { --color-primary: ... }`
3. **Existing components** - Scan 2-3 components for color usage patterns
**Typography:**
1. **Tailwind config** - Check `theme.extend.fontFamily`
2. **Font imports** - Look in layout files or CSS (Next.js `next/font`, Google Fonts, local fonts)
3. **CSS variables** - Check for `--font-sans`, `--font-heading`
4. **Existing components** - Identify font usage patterns
**Other patterns:**
- Spacing scale (p-4, mb-6, etc.)
- Border radius (rounded-lg, rounded-xl)
- Shadows (shadow-md, shadow-lg)
- Interactive states (hover, focus colors)
### Detecting Tailwind Version (CRITICAL)
**ALWAYS check the Tailwind CSS version before writing utility classes.**
Tailwind v3 and v4 have different syntax, and mixing them causes errors.
**How to detect version:**
1. **Check `package.json`**: Look for `"tailwindcss": "^3.x.x"` or `"tailwindcss": "^4.x.x"`
2. **Check config file**:
- v3: Uses `tailwind.config.js/ts` with `module.exports` or `export default`
- v4: May use CSS-based config with `@import "tailwindcss"`
3. **Check existing components**: Look at class usage patterns
**Key differences:**
**Tailwind v3:**
```tsx
// v3 syntax
<div className="bg-primary text-white">Content</div>
```
**Tailwind v4:**
```tsx
// v4 may use CSS variables differently
// Check the project's existing patterns
<div className="bg-primary text-white">Content</div>
```
**Common mistake:** Using v3 syntax in v4 projects or vice versa. Always verify the version first.
### Document Discovery
Create mental inventory of:
- **Primary color(s)** and their usage
- **Font families** (sans, serif, heading, mono)
- **Common patterns** (button styles, card designs, spacing)
- **Semantic names** (primary, secondary, accent vs blue-500, red-600)
## Critical Consistency Rules
### ALWAYS Follow These Rules
**NEVER use emojis in storefront UI** - Always use icons or images instead
```tsx
// ✅ CORRECT - Using icon component or image
<button className="flex items-center gap-2">
<ShoppingCartIcon className="w-5 h-5" />
Add to Cart
</button>
// ❌ WRONG - Using emoji
<button>
🛒 Add to Cart
</button>
```
**Why:** Emojis appear differently across platforms, lack professional appearance, and can cause accessibility issues. Use icon libraries (Heroicons, Lucide, Font Awesome) or SVG images instead.
**USE existing design tokens** (colors, fonts, spacing from theme)
```tsx
// ✅ CORRECT - Using theme colors
<button className="bg-primary text-white hover:bg-primary-dark">
Click Me
</button>
// ❌ WRONG - Arbitrary colors when theme exists
<button className="bg-[#3B82F6] text-white hover:bg-[#2563EB]">
Click Me
</button>
```
**USE existing font definitions**, not new font families
```tsx
// ✅ CORRECT - Using theme font
<h1 className="font-heading text-4xl font-bold">
Welcome
</h1>
// ❌ WRONG - Introducing new font
<h1 className="font-['Montserrat'] text-4xl font-bold">
Welcome
</h1>
```
**MATCH patterns from existing components**
```tsx
// If existing buttons use: bg-primary px-6 py-3 rounded-lg
// New buttons should use the same pattern
<button className="bg-primary px-6 py-3 rounded-lg">
New Button
</button>
```
### NEVER Do These Things
**DON'T introduce new colors without user approval**
- If you need a color not in the theme, ASK first
- Don't use arbitrary values like `bg-[#FF6B6B]` when theme has colors
**DON'T add new fonts without user approval**
- If current design uses Inter, don't add Montserrat without asking
- Don't use `font-['NewFont']` syntax when theme fonts exist
**DON'T use hard-coded values when theme tokens exist**
- Use `bg-primary` not `bg-[#3B82F6]`
- Use `p-6` not `p-[24px]`
- Use `font-heading` not `font-['Poppins']`
**DON'T create inconsistent patterns**
- If buttons use `rounded-lg`, all buttons should
- If cards use `shadow-md`, all cards should
- If hover effects use `hover:bg-primary-dark`, be consistent
## When to Ask User Approval
**ALWAYS ask before:**
### 1. Adding New Color
```
"I notice the current palette doesn't include an orange accent color.
Should I add one, or would you prefer to use the existing accent color?"
```
**Scenario:** You're building a promotional banner that needs an orange color, but theme only has blue/purple.
### 2. Adding New Font
```
"The current design uses Inter for all text. Do you want me to add
a different font for headings, or keep using Inter throughout?"
```
**Scenario:** Building a hero section and wondering if headings should use a different font.
### 3. Changing Existing Definitions
```
"Should I update the primary color to #3B82F6, or create a
new color variant?"
```
**Scenario:** Current primary is #2563EB but new design mockup shows #3B82F6.
### 4. Creating New Pattern
```
"The current components don't have a ghost button style (transparent with border).
Should I create one, or use an existing button variant?"
```
**Scenario:** Need a subtle button style that doesn't exist yet.
### DON'T Ask About
❌ Standard web dev decisions (responsive breakpoints, hover effects)
❌ Component structure or layout choices
❌ Accessibility patterns (AI agents know WCAG)
❌ Using existing theme colors/fonts in new ways
## New Project Setup
When starting a new project WITHOUT existing theme:
### Ask User These Questions
**1. Brand Colors:**
```
"What are your brand colors? Please provide:
- Primary color (main brand color)
- Secondary color (optional)
- Any specific hex codes or color preferences?"
```
**2. Font Preferences:**
```
"Do you have font preferences?
- Modern and clean (Inter, Poppins)
- Classic and professional (Merriweather, Lora)
- Specific fonts?
- Or should I choose appropriate fonts?"
```
**3. Design Style:**
```
"What design style do you prefer?
- Minimal (lots of whitespace, clean lines)
- Bold (vibrant colors, large typography)
- Professional (conservative, trust-focused)
- Modern (rounded corners, gradients, shadows)"
```
**4. Reference Sites (Optional):**
```
"Do you have 2-3 example websites you like the look of?
This helps me understand your aesthetic preferences."
```
### Setup Theme Configuration
After gathering preferences, configure Tailwind theme:
```typescript
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
primary: '#3B82F6', // User's primary color
secondary: '#8B5CF6', // User's secondary
accent: '#F59E0B', // Accent if needed
// Full scales if sophisticated design
brand: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
heading: ['Poppins', 'sans-serif'],
},
},
},
}
```
**Use Tailwind CSS for all new projects** - industry standard for ecommerce, highly customizable, excellent DX.
## Decision Tree
**When creating any component:**
```
1. Does a theme configuration exist?
├─ Yes → Extract colors/fonts from theme
│ Use existing tokens for new component
└─ No → Ask user for brand preferences
Create theme configuration
2. Are there similar existing components?
├─ Yes → Follow their patterns exactly
│ (spacing, colors, hover states)
└─ No → Check ANY existing components
Extract general patterns (spacing scale, hover effects)
3. Do you need a color/font not in theme?
├─ Yes → ASK user for approval before adding
│ Explain why you need it
└─ No → Proceed with existing tokens
4. Are you unsure about a design pattern?
├─ Yes → Check 2-3 existing components for guidance
│ Follow majority pattern
└─ No → Implement using theme tokens
Maintain consistency with existing components
```
## Common Mistakes
### ❌ Using Arbitrary Values When Theme Exists
**Problem:** Using `bg-[#3B82F6]` when `bg-primary` exists.
**Why it's wrong:** Bypasses theme, creates inconsistency, harder to maintain.
**Fix:** Always use semantic names from theme.
### ❌ Introducing New Colors Without Permission
**Problem:** Adding `text-orange-500` when theme doesn't have orange.
**Why it's wrong:** User may not want orange in their brand, creates color chaos.
**Fix:** Ask user first: "Should I add an orange color, or use existing accent?"
### ❌ Not Checking Existing Patterns
**Problem:** Creating buttons with `rounded-full` when all other buttons use `rounded-lg`.
**Why it's wrong:** Visual inconsistency confuses users.
**Fix:** Check 2-3 existing buttons, use same rounding.
### ❌ Adding Fonts Without Permission
**Problem:** Using `font-['Montserrat']` when theme uses Inter everywhere.
**Why it's wrong:** Fonts are brand identity - can't arbitrarily change.
**Fix:** Use existing `font-heading` or `font-sans`, or ask to add Montserrat.
### ❌ Using Inline Styles Instead of Theme
**Problem:** `style={{ backgroundColor: '#3B82F6', padding: '24px' }}`
**Why it's wrong:** Bypasses Tailwind theme, not responsive, harder to maintain.
**Fix:** Use Tailwind classes: `bg-primary p-6`
### ❌ Mixing Tailwind v3 and v4 Syntax
**Problem:** Using Tailwind v3 syntax in a v4 project, or vice versa.
**Why it's wrong:** Different versions have different configuration and syntax patterns. Mixing them causes build errors and unexpected styling behavior.
**Fix:** Check `package.json` for Tailwind version first. Look at existing components to understand the syntax patterns used in the project. Match the version-specific patterns consistently.
### ❌ Inconsistent Interactive States
**Problem:** Some buttons use `hover:bg-primary-600`, others use `hover:brightness-110`.
**Why it's wrong:** Inconsistent user experience.
**Fix:** Check existing buttons, use same hover pattern everywhere.
### ❌ Creating Theme Changes Without Approval
**Problem:** Adding new color to `tailwind.config.ts` without asking.
**Why it's wrong:** Theme changes affect entire project, need user agreement.
**Fix:** Ask first, explain rationale, get approval.
## Summary Checklist
**Before creating any component:**
- [ ] **Detected Tailwind CSS version (v3 or v4) from package.json**
- [ ] Checked for existing theme configuration (Tailwind config or CSS variables)
- [ ] Extracted existing colors and documented them
- [ ] Extracted existing fonts and documented them
- [ ] Reviewed 2-3 existing components for patterns
- [ ] Identified spacing scale, border radius, shadow patterns
- [ ] Confirmed I'm using theme tokens, not arbitrary values
- [ ] Matched hover/focus states from existing components
- [ ] Verified color contrast meets WCAG 2.1 AA (4.5:1 for text) - Use [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [ ] Asked user before adding any new colors or fonts
- [ ] Maintained visual consistency across all components
**This is about CONSISTENCY, not creating new designs.** Match what exists, ask before changing.

View File

@@ -0,0 +1,307 @@
# Promotions Feature
## Contents
- [Overview](#overview)
- [Promotion Types and When to Use](#promotion-types-and-when-to-use)
- [Sale Price Display](#sale-price-display)
- [Promo Code Input](#promo-code-input)
- [Free Shipping Threshold](#free-shipping-threshold)
- [Promotional Banners](#promotional-banners)
- [Countdown Timers](#countdown-timers)
- [Mobile Considerations](#mobile-considerations)
- [Checklist](#checklist)
## Overview
Promotions are temporary price reductions, discounts, or special offers designed to drive sales and incentivize purchases. Effective promotion UI clearly communicates value, creates urgency, and makes redemption easy.
**Backend Integration (CRITICAL):**
All promotion logic and data must come from the ecommerce backend. Do this based on backend integrated. Fetch active promotions, discount codes, and price rules from backend API. Never hardcode promotion logic in frontend.
### Key Ecommerce Requirements
- Clear discount communication (strikethrough pricing, percentage off)
- Promo code input (cart/checkout)
- Free shipping threshold progress (increase AOV)
- Countdown timers (create urgency)
- Automatic discount application
- Sale badges (product discovery)
### Purpose
**Conversion optimization:**
- Drive sales and increase conversion rate
- Increase average order value (free shipping thresholds, tiered discounts)
- Acquire new customers (first-order discounts)
- Create urgency (limited-time offers)
- Clear inventory (seasonal sales)
- Reward loyalty (VIP codes, member discounts)
## Promotion Types and When to Use
### Sales (Price Reductions)
**What it is**: Select products with reduced prices, automatically applied. No code needed.
**Use when:**
- Seasonal sales (Black Friday, holiday sales)
- Clearance or end-of-season inventory
- Product-specific promotions
- You want reduced prices visible on product pages (increases click-through)
**Display:**
- Strikethrough original price (provides context for savings)
- Sale price bold and prominent (red or brand color)
- Sale badge on product cards ("Sale", "30% Off")
**Medusa implementation:**
Use Price Lists with special prices for products. Provides automatic strikethrough pricing in cart and on product pages.
### Discount Codes
**What it is**: Customer enters code to unlock discount (percentage, fixed amount, or free shipping).
**Use when:**
- Newsletter signups ("Get 10% off with WELCOME10")
- VIP or loyalty program members (exclusive codes)
- Targeted marketing campaigns (email, social media)
- First-time customer incentives
- Friends and family discounts (limited distribution)
**Display:**
- Promo code input field in cart/checkout
- Success message: "Code applied: WELCOME10"
- Discount shown in order summary with code name
- Remove option (X icon or "Remove" link)
**Medusa implementation:**
Discount/promo code system with advanced logic (order-level discounts, usage limits, expiration dates).
### Automatic Discounts
**What it is**: Discount automatically applied when conditions met. No code entry required.
**Use when:**
- Free shipping thresholds ("Free shipping over $50")
- Volume discounts ("Spend $100, get $20 off")
- Buy One Get One (BOGO) offers
- Encouraging larger cart values (increase AOV)
**Display:**
- Banner announcing the promotion
- Progress indicator toward threshold (see Free Shipping Threshold section)
- "Discount applied" message in cart
- Automatic addition to order summary
### Buy X, Get Y (BOGO)
**What it is**: Purchase certain products to unlock free/discounted items.
**Use when:**
- Moving inventory (clear out slow-moving products)
- Cross-selling related products ("Buy sunscreen, get beach bag 50% off")
- Increasing units per transaction
**Display:**
- Clear promotion text on product page ("Buy 2, Get 1 Free")
- Free/discounted item shown in cart with explanation
- Discount line in order summary
**Medusa implementation:**
Buy X Get Y automatic discount. Free/discounted item must be added to cart to activate.
## Sale Price Display
### Strikethrough Pricing Pattern
**Format:**
```
$49.99 $34.99
(Original, strikethrough) (Sale price, bold)
```
**Design:**
- Original price: Strikethrough, muted gray color, smaller
- Sale price: Bold, larger, red or accent color
- Clear visual hierarchy (sale price dominates)
**Placement:**
- Product cards: Below image
- Product page: Near "Add to Cart" button
- Cart: Item price column
### Percentage Off Display
Show savings to emphasize value.
**Options:**
- "Save 30%" badge
- "30% off" label
- "$15 off" (absolute savings)
**Placement:**
- Badge on product image (top-left or top-right corner)
- Near price (inline or below)
- In cart summary ("Total savings: $45")
### Sale Badge
Bright badge on product image (red, orange, yellow) in top corner. 48-64px desktop, 40-48px mobile. Text: "Sale", "30% Off", or "Save $15".
## Promo Code Input
### Placement and Design
**Location:**
Cart page order summary or checkout page. Position in right sidebar (desktop) or below items (mobile).
**Layout:**
- Label: "Promo code" or "Discount code"
- Text input (200-280px desktop, full-width mobile)
- "Apply" button inline or stacked (mobile)
- Auto-uppercase on submit (codes usually uppercase)
**Expandable pattern (optional):**
"Have a promo code?" link that expands to show input. Saves vertical space, reduces visual clutter.
### Success and Error States
**Success:**
- Green checkmark or success message: "Code applied: WELCOME10"
- Discount shown in order summary with code name: "Discount (WELCOME10) -$10.00"
- Remove option: X icon or "Remove" link
- Update cart total immediately
**Error:**
- Red error message below input: "Invalid code", "Code expired", or "Minimum purchase not met"
- Input remains visible for retry
- Don't clear input field (user may have typo)
**Applied code display in order summary:**
```
Subtotal $100.00
Discount (WELCOME10) -$10.00
Shipping $5.00
─────────────────────
Total $95.00
```
## Free Shipping Threshold
**Purpose (CRITICAL)**: Increase average order value by encouraging customers to add more items to reach free shipping.
### Progress Bar Pattern
**Display in cart:**
- "Add $25 more for FREE SHIPPING"
- Horizontal progress bar showing proximity to threshold
- Updates automatically as cart value changes
- Green when threshold reached
**Example:**
```
Add $25.00 more for FREE SHIPPING
[███████░░░░░░░░] 50%
```
**When threshold met:**
- "You've unlocked free shipping!" (success message)
- Green checkmark or badge
- Crossed-out shipping charge in order summary
**Why it works:**
- Visualizes proximity to goal (loss aversion)
- Increases AOV by 15-30% on average
- Reduces cart abandonment (free shipping is top reason to complete purchase)
### Free Shipping Banner
Sitewide announcement: "Free shipping on orders over $50". Display in top banner or near cart icon. Visible on all pages for awareness.
## Promotional Banners
### Top Banner
Full-width strip at top of page (48-64px height). Bright color contrasting with navbar. Short message: "Free shipping on orders over $50" or "Sale: Up to 50% off - Shop Now".
**Position:**
- Above navbar (most common)
- Below navbar (alternative)
- Sticky (stays visible on scroll, optional)
**CTA:**
Link to sale page ("Shop Now", "Learn More") or whole banner clickable.
### Hero Banner
Large hero section on homepage with promotional message. Background image, headline ("Black Friday Sale"), subheading ("Up to 60% off sitewide"), CTA button ("Shop the Sale"), optional countdown timer.
### Inline Banners
Within page content (product pages, cart). Examples: Free shipping reminder on cart page, "Sale ends soon" on product page. Less prominent than hero.
## Countdown Timers
Use for time-sensitive promotions to create urgency and FOMO.
**When to use:**
- Flash sales (24-hour sales)
- Limited-time offers
- Holiday promotions
- Never for permanent sales (fake urgency harms trust)
**Display format:**
- "Sale ends in: 2d 14h 32m 15s"
- Or simpler: "Ends in 2 days"
- Or: "Hurry! Only 14 hours left"
**Placement:**
Top banner, product page near price, cart page, or hero section.
**Implementation:**
Server-side time to prevent client manipulation, auto-hide when expired, update in real-time.
## Mobile Considerations
**Top banner:**
Shorter text (fewer words), smaller height (40-48px), dismissible (X button).
**Sale badges:**
Slightly smaller (40-48px), still clearly visible, don't obstruct product image.
**Promo code input:**
Full-width input and button, stacked layout (input above button), large touch targets (48px height), expandable section to save space.
**Countdown timer:**
Simplified format ("Ends in 14 hours" vs full d:h:m:s), larger text for readability.
## Checklist
**Essential features:**
- [ ] Strikethrough original price for sales
- [ ] Sale price bold, prominent, colored
- [ ] Sale badges on product images (40-64px)
- [ ] Percentage off displayed ("30% Off")
- [ ] Promo code input field in cart/checkout
- [ ] "Apply" button next to promo input
- [ ] Success message after applying code
- [ ] Error message for invalid codes
- [ ] Applied code displayed in order summary with name
- [ ] Remove code option (X icon or "Remove" link)
- [ ] Total savings highlighted in cart
- [ ] Free shipping progress bar (if applicable)
- [ ] Progress updates as cart value changes
- [ ] Success message when threshold met
- [ ] Countdown timer for time-limited offers (server-side)
- [ ] Promotional banners (top banner, hero)
- [ ] Backend integration (fetch promotions from API)
- [ ] Mobile: Full-width promo input, stacked layout
- [ ] Mobile: Large touch targets (48px)
- [ ] Expandable promo section (optional, saves space)
- [ ] ARIA labels on promo input
- [ ] Screen reader announcements for price changes
- [ ] Keyboard accessible (Tab, Enter)
- [ ] High contrast text (4.5:1 minimum)

View File

@@ -0,0 +1,230 @@
# Wishlist Feature
## Contents
- [Overview](#overview)
- [Backend Support Check](#backend-support-check)
- [Wishlist Button](#wishlist-button)
- [Adding and Removing](#adding-and-removing)
- [Wishlist Page](#wishlist-page)
- [Guest vs Logged-In Users](#guest-vs-logged-in-users)
- [Navigation Icon](#navigation-icon)
- [Mobile Considerations](#mobile-considerations)
- [Checklist](#checklist)
## Overview
A wishlist (also called favorites or save for later) allows customers to save products they're interested in for future purchase. This feature helps organize shopping, track desired items, and increases return visits and conversions.
### Key Ecommerce Benefits
**Why wishlists matter:**
- Increase return visits (users come back to check wishlist)
- Reduce cart abandonment (save for later instead of abandoning)
- Gift planning (save items for gift lists, share with others)
- Price tracking (users monitor items for sales - remarketing opportunity)
- Engagement metric (shows product interest for analytics)
**Conversion impact:**
- Users with wishlists have 2-3x higher lifetime value
- Wishlist-to-purchase conversion: 20-30% on average
- Email reminders about wishlist items: 15-25% click-through rate
## Backend Support Check
**CRITICAL: Only implement wishlist UI if your ecommerce backend supports wishlist functionality.**
Before implementing:
1. **Check backend API** - Verify wishlist endpoints exist (or ask user)
2. **Authentication** - Confirm if login required for wishlist storage
3. **Test operations** - Verify add/remove/fetch functionality works
**Medusa users:**
Medusa core doesn't include wishlist by default. Install the Wishlist plugin from Medusa examples repository. Plugin provides full wishlist functionality with API endpoints.
**General backends:**
Wishlist typically requires user authentication. API endpoints needed:
- GET /wishlist (fetch user's wishlist)
- POST /wishlist (add item)
- DELETE /wishlist/{id} (remove item)
**If backend doesn't support wishlist:**
Don't implement the feature. localStorage-only wishlist creates poor UX (lost on device switch, no sync, no remarketing).
## Wishlist Button
### Design and States
**Heart icon** (universal symbol):
- Outline heart: Not in wishlist
- Filled heart: In wishlist
- 24-32px on product cards, 32-40px on product page
**Colors:**
- Outline: Gray or black
- Filled: Red, pink, or brand color
- High contrast against product image
### Placement
**Product cards:**
Top-right corner of product image, always visible (not hover-only), 16px margin from edges.
**Product detail page:**
Near "Add to Cart" button, or above product image, or with sharing options. Optional text label: "Add to Wishlist" or icon-only.
## Adding and Removing
### Adding to Wishlist
**Flow:**
1. User clicks heart icon
2. Show loading state briefly
3. Send API request to add item
4. Update icon to filled state
5. Show success feedback (toast: "Added to wishlist" or subtle animation)
6. Update navigation wishlist badge (+1)
**Optimistic UI:**
Update icon immediately, revert if API fails. Provides instant feedback.
**Error handling:**
Show error toast ("Failed to add to wishlist"), revert icon to outline, allow retry.
**Variant handling:**
Save specific variant (size, color) if selected on product page. On product cards, save default variant.
### Removing from Wishlist
**From product card/page:**
Click filled heart → changes to outline → toast: "Removed from wishlist" → update badge (-1).
**From wishlist page:**
X icon in corner of product card or "Remove" button → item fades out. Optional: Undo action in toast (5 seconds).
**Confirmation:**
Generally not needed (low stakes, easily reversible). Only confirm for bulk actions ("Clear all").
## Wishlist Page
### Layout
**Heading:**
"My Wishlist" or "Favorites" with item count ("12 items saved").
**Product grid:**
Similar to product listing page. Product cards with images, titles, current prices (may differ from when added), stock status.
**Empty state:**
"Your wishlist is empty" with "Start Shopping" CTA.
### Product Card Information
Display per item:
- Product image (linked to product page)
- Product title (linked)
- Current price (may show sale price if on sale now)
- Original price if on sale (strikethrough)
- Variant details (size, color if saved)
- Stock status: In stock (green), Out of stock (red, "Notify me" option), Low stock ("Only 2 left")
- **"Add to Cart" button** (CRITICAL - conversion path)
- Remove button (X icon)
### Actions and Conversion
**Add to Cart (CRITICAL):**
"Add to Cart" button on each item. Adds item to cart **without removing from wishlist** (user may want both). Success toast: "Added to cart". Don't navigate away (stay on wishlist page).
**Tradeoff:**
- **Keep in wishlist** (recommended): User tracks desired items, can reorder easily
- **Move to cart**: Removes from wishlist after adding - simpler but limits reordering
**Stock handling:**
If out of stock, disable "Add to Cart" and show "Notify me when back in stock" option (if backend supports).
## Guest vs Logged-In Users
### Decision: Require Login or Use localStorage?
**Require login (Recommended):**
**Why:**
- Wishlist requires persistent storage across devices
- Enables email reminders and price drop notifications
- Better user experience (never lost)
- Cleaner data for analytics and remarketing
- Avoids confusion of lost wishlist items
**Implementation:**
Click wishlist → Show login prompt modal: "Log in to save your wishlist". Include "Sign Up" button. Clear benefit: "Save items across all your devices".
**localStorage approach (Not Recommended):**
- Device-specific only (lost on device switch)
- Lost if user clears browser data
- No remarketing opportunities
- No email reminders
- Creates poor UX expectations
**Exception:**
If backend doesn't support authenticated wishlist, consider not implementing feature at all rather than localStorage-only.
## Navigation Icon
### Placement and Design
**Position:**
Top navigation bar, between search icon and cart icon. Or: In user account dropdown menu.
**Icon:**
Heart icon (outline or filled if items in wishlist). 24-32px size, consistent with cart icon.
**Badge count:**
Small circle with number showing total items in wishlist. Red or brand color, positioned top-right of heart icon.
**Link behavior:**
Navigates to wishlist page on click. Dropdown less common for wishlist (unlike cart popup).
## Mobile Considerations
**Heart button:**
Larger touch target (44px minimum), positioned in corner of product image, clear tap feedback (scale or color change).
**Wishlist page:**
Single column product grid, stack cards vertically, full-width "Add to Cart" buttons, large remove buttons (44px touch target).
**Navigation icon:**
Heart icon in mobile navbar or hamburger menu, with badge count.
**Login prompt:**
If guest clicks wishlist, show bottom sheet (less disruptive than full modal) with "Log in to save your wishlist" message.
## Checklist
**Essential features:**
- [ ] Backend API support verified before implementing
- [ ] Heart icon on product cards (top-right corner)
- [ ] Heart icon on product detail page
- [ ] Clear filled vs outline states
- [ ] Toast notification on add/remove
- [ ] Wishlist icon in navigation with badge count
- [ ] Wishlist page with product grid
- [ ] Product info: image, title, current price, stock status
- [ ] Variant details if saved (size, color)
- [ ] "Add to Cart" button on each wishlist item
- [ ] Add to cart without removing from wishlist
- [ ] Remove button (X icon) on each item
- [ ] Empty wishlist state ("Start Shopping" CTA)
- [ ] Login required for persistent wishlist
- [ ] Guest user login prompt on wishlist click
- [ ] Stock status indicators (in stock, out of stock, low stock)
- [ ] Out of stock: Disable add to cart, show "Notify me"
- [ ] Mobile: 44px touch targets
- [ ] Mobile: Single column layout
- [ ] Optimistic UI (instant feedback)
- [ ] Error handling for failed API requests
- [ ] Loading states during add/remove
- [ ] Button aria-label ("Add to wishlist" / "Remove from wishlist")
- [ ] aria-pressed attribute on heart button
- [ ] Keyboard accessible (Tab, Enter/Space)
- [ ] Screen reader announcements for add/remove

View File

@@ -0,0 +1,380 @@
# Account Pages Layout
## Contents
- [Overview](#overview)
- [Account Dashboard](#account-dashboard)
- [Order Management](#order-management)
- [Saved Addresses](#saved-addresses)
- [Payment Methods](#payment-methods)
- [Profile and Security](#profile-and-security)
- [Email Preferences](#email-preferences)
- [Navigation and Layout](#navigation-and-layout)
- [Mobile Considerations](#mobile-considerations)
- [Checklist](#checklist)
## Overview
Account pages allow customers to manage orders, save addresses, update preferences, and view order history. Well-designed account pages improve repeat purchase rates and reduce support inquiries.
**Backend Integration (CRITICAL):**
All customer data (orders, addresses, profile, payment methods) must be fetched from the ecommerce backend. Change this based on backend integrated. Never hardcode or mock account data. Consult backend documentation for:
- Customer data endpoints (profile, preferences)
- Order history and details endpoints
- Address CRUD operations
- Payment method storage (if supported)
- Authentication requirements
### Key Ecommerce Requirements
- Order history with status tracking (builds trust)
- Saved addresses (checkout optimization - reduces friction)
- Reorder functionality (increases repeat purchases)
- Order tracking integration
- Email preference controls (compliance and user control)
- Secure authentication and session management
### Purpose
**Primary ecommerce functions:**
- Reduce checkout friction (saved addresses, payment methods)
- Increase repeat purchases (order history, reorder button)
- Reduce support load (order tracking, self-service returns)
- Build trust (order transparency, delivery updates)
- Retain customers (easy account management)
## Account Dashboard
Landing page after login. Purpose: Quick access to recent activity and common actions.
**Display (prioritize recent orders):**
- Welcome message with customer name
- Recent orders (3-5 most recent with status)
- Quick actions: Track order, Reorder, Manage addresses
- Account summary (saved addresses count, loyalty points)
**Reorder functionality (CRITICAL for repeat purchases):**
- Check first that feature is available in the admin.
- "Reorder" button on each order card
- Adds same items to cart (check stock availability first)
- Success feedback (cart updated with X items)
- Don't navigate away (stay on dashboard)
**Example dashboard:**
```
Welcome back, Sarah!
Recent Orders
- Order #12345 - Delivered (Jan 28) - $89.99 [Reorder]
- Order #12344 - In Transit (Jan 27) - $124.50 [Track Order]
- Order #12343 - Processing (Jan 26) - $45.00
[View All Orders →]
Quick Actions
[Track Order] [Manage Addresses] [Contact Support]
```
## Order Management
### Order History
Display all past orders with filtering and search.
**Order card essentials:**
- Order number (clickable to details page)
- Order date and status badge (Processing, Shipped, Delivered)
- Total amount
- First 2-3 product thumbnails
- Quick actions: Track, View Details, Reorder, Invoice
**Status indicators (color-coded):**
- Processing: Yellow/Orange
- Shipped: Blue
- Delivered: Green
- Cancelled: Gray/Red
**Filtering and search:**
- Date range (Last 30 days, Last 6 months, All time)
- Status filter (All, Processing, Shipped, Delivered)
- Search by order number or product name
**Sorting:**
- Most recent first (default)
- Oldest first
- Highest/lowest price
**Pagination:**
Show 10-20 orders per page with pagination controls. Alternative: "Load More" button (better mobile UX).
### Order Details View
Full order information page.
**Display:**
- Order number, date, status with progress timeline
- Tracking number with carrier link (if shipped)
- Estimated delivery date
**Status timeline (builds trust):**
```
✓ Order Placed (Jan 27, 9:45 AM)
✓ Processing (Jan 27, 10:30 AM)
✓ Shipped (Jan 28, 2:15 PM)
○ Out for Delivery
○ Delivered
```
**Order information:**
- Items ordered (image, name, variant, quantity, price)
- Pricing breakdown (subtotal, shipping, tax, discounts, total)
- Shipping address and method
- Billing address
- Payment method (last 4 digits)
**Order actions:**
- Track shipment (link to carrier tracking page)
- Download invoice/receipt (PDF)
- Request return (if eligible and backend supports)
- Reorder items
- Contact support about order
### Reorder Functionality (Ecommerce-Specific)
**Purpose**: Increase repeat purchases by making it easy to reorder past purchases.
**Implementation:**
- "Reorder" button on order cards and order details
- Check stock availability before adding to cart
- Handle discontinued products gracefully (skip or notify)
- Add all available items to cart
- Success message: "5 items added to cart" (or "3 of 5 items added - 2 unavailable")
- Stay on current page (don't navigate away)
**Tradeoff**: Auto-add to cart (friction-free) vs redirect to cart page (let user review first). Recommend auto-add with clear success feedback.
## Saved Addresses
**Purpose (CRITICAL)**: Reduce checkout friction and increase conversion. Saved addresses make repeat purchases faster and easier.
### Why Addresses Matter
**Conversion optimization:**
- Saved addresses reduce checkout time by 50%+ (no retyping)
- Default address selection streamlines checkout flow
- Reduces form abandonment (fewer fields to fill)
- Increases repeat purchase rate (easier checkout)
**Backend integration:**
Fetch, create, update, and delete addresses via backend API. Do this based on backend integrated.
### Address Book Display
**Saved addresses list:**
- All saved addresses
- Default address indicator (badge: "Default Shipping" or star icon)
- Address preview: Name, street, city, state, zip
- Quick actions: Edit, Delete, Set as Default
**Default address behavior:**
- One default shipping address
- One default billing address (separate or same)
- Used automatically at checkout (user can change)
- Setting new default updates previous default
### Add/Edit Address Form
Collect standard shipping information. Key considerations:
**Required fields:**
- Full name (or first + last)
- Address line 1
- City, State/Province, ZIP/Postal code
- Country
- Phone number (recommended for delivery coordination)
**Optional enhancements:**
- Address label (Home, Work) for easy identification
- Address autocomplete API (Google Places) for accuracy
- "Set as default" checkbox
**Validation:**
Real-time validation, especially for ZIP/postal code format based on country.
## Payment Methods
**Note**: Payment method storage is optional. Only implement if:
- Backend securely handles tokenized payment data
- PCI DSS compliance requirements are met
- Payment gateway supports tokenization (Stripe, Braintree)
**Security (CRITICAL):**
- Never store full card numbers (tokenize with payment gateway)
- Display last 4 digits only
- Don't store CVV
- Use payment gateway hosted forms (Stripe Elements, etc.)
- Show "Securely stored" badge for trust
**Saved payment display:**
- Card type logo (Visa, Mastercard)
- Last 4 digits
- Expiration date
- Default indicator
- Actions: Edit (update expiration/billing address), Delete, Set as Default
**Tradeoff**: Saved payment methods increase convenience but require PCI compliance. If not implemented, users enter payment at checkout each time (more friction but simpler backend).
## Profile and Security
### Profile Information
Display and edit customer information.
**Standard fields:**
- Full name
- Email (with verification status)
- Phone number
- Optional: Date of birth, gender
**Edit functionality:**
Inline editing or separate form, real-time validation, success confirmation.
**Email verification:**
If unverified, show warning with "Resend verification email" button. If verified, show checkmark badge.
### Security Settings
**Password change:**
- Require current password (optional)
- New password with strength indicator
- Confirm new password
- Password requirements display (8+ chars, uppercase, number)
**Two-factor authentication (optional):**
Enable/disable 2FA, setup instructions, backup codes. Only implement if backend supports.
## Email Preferences
Ecommerce-specific email controls.
**Preference categories:**
1. **Transactional emails** (order updates, shipping) - Recommended always enabled, may be legally required
2. **Marketing emails** (sales, promotions, new products) - User choice
3. **Newsletter** (weekly roundup, content) - User choice
**Display:**
Checkbox list or toggle switches with clear descriptions. Save button at bottom.
**Example:**
```
Email Preferences
[✓] Order and shipping updates
Receive confirmations and tracking info
[ ] Marketing emails
Sales, promotions, and new products
[ ] Newsletter
Weekly roundup and articles
[Save Preferences]
```
**Unsubscribe:**
Individual opt-outs per type, "Unsubscribe from all marketing" button. Keep transactional emails enabled (required for order fulfillment).
## Navigation and Layout
### Layout Pattern Decision
Choose based on account complexity:
**Sidebar Navigation (Recommended):**
- **Use when**: 6+ account sections, complex account features
- Desktop: Vertical sidebar (20-25% width) with section links
- Mobile: Collapse to hamburger menu or dropdown
- Benefits: Persistent navigation, professional, accommodates many sections
**Tab Navigation:**
- **Use when**: 4-6 account sections, simpler account structure
- Horizontal tabs at top, active tab highlighted
- Mobile: Horizontal scroll or dropdown
- Benefits: Modern, clean, quick switching
**Account Hub (Mobile-First):**
- **Use when**: Mobile-heavy traffic, simple account
- Landing page with section cards (2-column grid)
- Tap card to enter section, back button returns to hub
- Benefits: Touch-friendly, intuitive, minimal hierarchy
### Section Organization
**Recommended order (most to least used):**
1. Dashboard (landing page)
2. Orders (most accessed)
3. Addresses (important for checkout)
4. Payment Methods (if implemented)
5. Profile
6. Security
7. Email Preferences
8. Logout
## Mobile Considerations
**Mobile-specific patterns:**
**Navigation:**
Account hub with section cards (2 columns), or bottom navigation with 4-5 key sections (Orders, Addresses, Profile, More).
**Forms:**
One field per row, larger inputs (48px height), appropriate keyboard types (email, phone, numeric), autofill enabled.
**Order history:**
Simplified order cards, full-width buttons, "Load More" pagination (better than numbered pages on mobile).
**Saved addresses:**
Stacked address cards, full-width, 48px touch targets for edit/delete.
## Checklist
**Essential features:**
- [ ] Account dashboard with recent orders (3-5)
- [ ] Reorder button (adds items to cart, stays on page)
- [ ] Order history with status indicators
- [ ] Filter orders by date range and status
- [ ] Search orders by number or product name
- [ ] Order details page with tracking info
- [ ] Status timeline (Order Placed → Processing → Shipped → Delivered)
- [ ] Track shipment button (links to carrier)
- [ ] Download invoice/receipt option
- [ ] Saved addresses list with default indicator
- [ ] Add/edit/delete addresses with validation
- [ ] Set default address option
- [ ] Profile information edit
- [ ] Email verification status display
- [ ] Password change with strength indicator
- [ ] Current password required to change password
- [ ] Email preferences (transactional vs marketing)
- [ ] Account deletion option
- [ ] Logout button clearly visible
- [ ] Clear navigation between sections
- [ ] Mobile-responsive (single column, 48px touch targets)
- [ ] Backend integration (all data fetched from API)
- [ ] Success confirmations after saves
- [ ] Error handling with clear messages
- [ ] Keyboard accessible
- [ ] ARIA labels on navigation sections
- [ ] Order status announcements for screen readers
**Optional features:**
- [ ] Saved payment methods (if PCI compliant backend)
- [ ] Two-factor authentication
- [ ] Wishlist integration
- [ ] Loyalty points/rewards display
- [ ] Returns management section
- [ ] Address autocomplete API

View File

@@ -0,0 +1,316 @@
# Cart Page
## Contents
- [Overview](#overview)
- [Layout Patterns](#layout-patterns)
- [Cart Items Display](#cart-items-display)
- [Quantity Updates](#quantity-updates)
- [Order Summary](#order-summary)
- [Promo Code Input](#promo-code-input)
- [Checkout Button](#checkout-button)
- [Empty Cart State](#empty-cart-state)
- [Backend Integration](#backend-integration)
- [Mobile Cart](#mobile-cart)
- [Checklist](#checklist)
## Overview
The cart page displays all items a customer has added to their shopping cart. Purpose: Review items, modify cart, apply promotions, proceed to checkout. Critical conversion point.
**⚠️ CRITICAL: Always display variant details (size, color, material, etc.) for each cart item, not just product titles.**
### Key Ecommerce Functions
- Review items before purchase (reduces buyer's remorse)
- Update quantities or remove items (cart management)
- Apply promotional codes (increase order value)
- View pricing breakdown (transparency builds trust)
- Proceed to checkout (conversion path)
- Continue shopping if needed (reduce abandonment)
## Layout Patterns
### Two-Column Pattern (Desktop)
**Most common:**
- Left column (60-70%): Cart items list
- Right column (30-40%): Order summary (sticky)
- Below items: Promo code input, continue shopping
- Order summary stays visible during scroll
### Mobile Layout
Single column (stacked):
- Cart items
- Order summary
- Promo code input
- Checkout button (sticky at bottom)
- Continue shopping
## Cart Items Display
### Cart Item Card
**CRITICAL: Always display variant details for each cart item.**
Products with variants (size, color, material, style, etc.) must show the selected variant options. Without this, customers can't confirm they have the correct items in their cart.
**Essential information per item:**
- Product image (thumbnail, 80-120px desktop, 60-80px mobile)
- Product title (linked to product page)
- **Variant details (REQUIRED)**: Size, color, material, or other variant options selected
- Format: "Size: Large, Color: Black" or "Large / Black"
- Display below title, smaller gray text
- Show ALL selected variant options
- Unit price
- Quantity selector
- Line total (unit price × quantity)
- Remove button (X icon)
**Layout:**
Horizontal card (image left, details right), clear visual separation between items, adequate spacing (16-24px).
**Why variant details are critical:**
- Customer confirmation before checkout
- Prevents returns from wrong variant purchases
- Allows easy correction if wrong variant in cart
- Essential for clothing, shoes, configurable products
### Price Display
**Medusa pricing (CRITICAL):**
Medusa stores prices as-is (not in cents). Display prices directly without dividing by 100. Example: If Medusa returns 49.99, display $49.99 (not $0.4999). Different from Stripe which stores prices in cents.
**Sale prices:**
Show original price (strikethrough) and sale price prominently if on sale.
**Line total:**
Total for item (price × quantity), bold or larger font, update dynamically when quantity changes.
## Quantity Updates
### Quantity Selector
Standard +/- buttons with number display:
```
[-] [2] [+]
```
**Behavior:**
- Min: 1 (can't go below, or remove item instead)
- Max: Stock available or cart limit
- Manual input allowed (type number)
- Update on change (blur or button click)
- Show loading state briefly
- Update line total immediately
### Auto-Update (Recommended)
Changes apply immediately, no "Update Cart" button needed. Better UX, less friction. Show brief loading indicator. Update order summary automatically.
**Error handling:**
"Only X available" if exceeds stock, reset to max available quantity, show error message near item.
## Order Summary
### Summary Card
Position: Right column on desktop (sticky), below cart items on mobile, fixed width (300-400px desktop).
### Price Breakdown
**Line items:**
```
Subtotal (3 items): $149.97
Shipping: $9.99
Tax: $12.00
─────────────────────
Total: $171.96
```
**Subtotal:**
Sum of all cart items with item count.
**Shipping:**
Estimated shipping cost, or "Calculated at checkout" (if address needed), or "Free shipping" (if applicable). Show free shipping threshold progress (see promotions.md).
**Tax:**
Estimated tax or "Calculated at checkout" (if address needed).
**Total:**
Grand total (bold, larger font), most prominent number.
### Savings Display
If discounts applied:
- Show total savings: "You saved $20.00" (green text)
- Or: Discount line item in breakdown
- Positive reinforcement
## Promo Code Input
### Input Field Design
**Layout:**
Label ("Promo code" or "Discount code"), text input (200-280px desktop, full-width mobile), "Apply" button inline or stacked (mobile). Positioned below cart items or in order summary.
**Auto-uppercase:**
On submit (codes usually uppercase).
**Expandable pattern (optional):**
"Have a promo code?" link that expands to show input. Saves vertical space.
### Success and Error States
**Success:**
- Green checkmark or success message: "Code applied: WELCOME10"
- Discount shown in order summary: "Discount (WELCOME10) -$10.00"
- Remove option: X icon or "Remove" link
- Update cart total immediately
**Error:**
- Red error message below input: "Invalid code", "Code expired", or "Minimum purchase not met"
- Input remains visible for retry
- Don't clear input field
**See also:** [promotions.md](../features/promotions.md) for detailed promo code patterns.
## Checkout Button
### Button Design
**Prominence:**
Large, full-width button, brand primary color (high contrast), 48-56px height (easy to tap). Text: "Proceed to Checkout" or "Checkout". Icon optional (lock or arrow).
**Position:**
Bottom of order summary (desktop), fixed at bottom of screen (mobile, optional), always visible during scroll.
**States:**
Default enabled, hover with slight color change, loading with spinner, disabled if cart empty or error.
**Security Indicators (optional):**
Lock icon with "Secure Checkout", payment badges (Visa, Mastercard, PayPal), "SSL Encrypted" message near button.
## Empty Cart State
### Display
When cart is empty:
- Centered content
- Icon or illustration (empty shopping bag)
- Heading: "Your cart is empty"
- Subtext: "Start adding items to your cart"
- CTA button: "Continue Shopping" or "Browse Products"
**Additional elements:**
- Link to popular categories
- Recently viewed products (if available)
- Bestsellers or featured products
## Backend Integration
### Data Source (CRITICAL)
**Fetch from ecommerce backend:**
Cart stored in backend (persistent), fetch on page load, sync with backend on changes.
**When to fetch:**
- Page load (initial cart data)
- After adding/updating/removing items
- After applying promo codes
### State Management
**Client-side cart state:**
Store cart data in global state (React Context), keep cart ID in localStorage, update state after API responses, share cart state across components (page, popup, header badge).
**Cart ID persistence:**
```javascript
localStorage.setItem('cart_id', cartId)
```
Send cart ID with every cart API request, create new cart if ID doesn't exist, clear cart ID on checkout completion.
### TanStack Query for Cart Data
**Recommended** for efficient caching and revalidation:
**Benefits:**
Built-in caching with automatic revalidation, optimistic updates support, automatic refetching on focus/reconnect, loading and error states handled, query invalidation for cart updates.
**Configuration:**
Use `useQuery` for fetching cart data, set `staleTime` to 30-60 seconds, use `queryClient.invalidateQueries(['cart'])` after updates.
**See also:** [connecting-to-backend.md](../connecting-to-backend.md) for detailed backend integration patterns.
### Medusa Integration
Use `@medusajs/medusa-js` SDK:
- Cart endpoints: `/store/carts`, `/store/carts/{id}`
- Add to cart: POST `/store/carts/{id}/line-items`
- Update quantity: POST `/store/carts/{id}/line-items/{lineId}`
- Remove item: DELETE `/store/carts/{id}/line-items/{lineId}`
- Apply discount: POST `/store/carts/{id}/promotions`
**Response data:**
Cart ID, items (product details, variants, quantities), subtotal, tax, shipping, total, applied discounts, item availability status.
**Error handling:**
Network errors (show retry option), invalid cart ID (create new cart), out of stock (show error, prevent adding), API errors (user-friendly message).
## Mobile Cart
### Mobile Layout
**Structure:**
Full-width cart items (stacked), simplified item cards, order summary below items, sticky checkout button at bottom.
**Cart item cards:**
Smaller product images (60-80px), truncated product titles (1-2 lines), essential info only, quantity selector (smaller, 36-40px), remove button visible.
### Sticky Checkout Bar
**Bottom sticky bar:**
Fixed at bottom of screen, total amount visible, "Checkout" button (full-width), appears after scrolling (optional), always accessible.
**Design:**
```
[Total: $171.96] [Checkout]
```
**Touch-friendly:**
44px minimum touch targets, adequate spacing between buttons, large remove buttons (40px).
## Checklist
**Essential elements:**
- [ ] **CRITICAL: Cart items display variant details (size, color, etc.) - not just product title**
- [ ] Cart items with images, titles, variant options, prices
- [ ] Quantity selector (+/- buttons, 40-44px minimum)
- [ ] Remove button per item (X icon, clearly visible)
- [ ] Order summary (subtotal, shipping, tax, total)
- [ ] Promo code input with "Apply" button
- [ ] Applied discount displayed in summary
- [ ] "Remove" option for applied code
- [ ] Prominent "Checkout" button (48-56px height)
- [ ] Continue shopping link
- [ ] Empty cart state (icon, message, CTA)
- [ ] Trust signals (secure checkout, payment badges)
- [ ] Auto-update quantities (no "Update Cart" button)
- [ ] Undo option after removing item (toast notification)
- [ ] Mobile: Sticky checkout button at bottom
- [ ] Mobile: Simplified cart item cards
- [ ] Backend integration (fetch cart from API)
- [ ] Cart ID persistence (localStorage)
- [ ] Real-time price updates
- [ ] Loading states (skeleton or spinner)
- [ ] Optimistic updates for quantity changes
- [ ] Stock availability warnings (if low stock)
- [ ] Free shipping threshold progress (if applicable)
- [ ] Keyboard accessible (Tab, Enter, Arrow keys)
- [ ] ARIA labels on quantity controls and buttons
- [ ] Screen reader announcements (aria-live)
- [ ] High contrast text (4.5:1 minimum)
- [ ] Error handling for failed updates

View File

@@ -0,0 +1,486 @@
# Checkout Page Layout
## Contents
- [Overview](#overview)
- [Decision: Single-Page vs Multi-Step](#decision-single-page-vs-multi-step)
- [Guest vs Logged-In Checkout](#guest-vs-logged-in-checkout)
- [Component Architecture](#component-architecture-recommended)
- [Checkout Flow](#checkout-flow)
- [Key Ecommerce Considerations](#key-ecommerce-considerations)
- [Backend Integration](#backend-integration)
- [Mobile Checkout](#mobile-checkout)
- [Trust and Conversion Optimization](#trust-and-conversion-optimization)
- [Error Handling](#error-handling)
- [Checklist](#checklist)
## Overview
Final step in conversion funnel where customers provide shipping and payment information. Must be optimized for completion with minimal friction.
**⚠️ CRITICAL: Always fetch shipping methods AND payment methods from backend. Users must be able to select from available options - never skip payment method selection.**
### Key Requirements
- Clear steps and progress indication
- Guest checkout option (if backend supports it)
- Shipping address and method selection
- **Shipping methods fetched from backend (vary by address/region)**
- **Payment methods fetched from backend (user must select preferred method)**
- Payment processing
- Order review before submission
- Trust signals throughout
- Mobile-optimized (60%+ traffic is mobile)
- Fast loading and submission
## Decision: Single-Page vs Multi-Step
**Use Single-Page Checkout when:**
- Simple products with few shipping options
- Mobile-heavy traffic (>60% mobile users)
- Fewer form fields required (<15 total)
- Startup or new store (minimize friction)
- Fast checkout is prioritized
- Low average order value (<$50)
**Benefits:**
- Fewer clicks (no step navigation)
- User sees full scope upfront
- Faster on mobile (no page loads)
- Lower perceived friction
**Use Multi-Step Checkout when:**
- Complex shipping (international, multiple carriers)
- B2B customers (need detailed information)
- Many form fields required (>15 total)
- High-value products (>$100, thoroughness expected)
- Established brand (customers trust process)
- Need clear progress indication
**Benefits:**
- Less overwhelming (one step at a time)
- Progress indicator reduces anxiety
- Easier step-by-step validation
- Better for complex forms
**Recommended: Hybrid Approach**
- Single-page scroll layout on desktop
- Accordion-based sections on mobile (expand/collapse)
- Progressive disclosure of sections
- Best of both worlds
**Common steps:**
1. Shipping Information (address)
2. Delivery (shipping method selection)
3. Payment (payment method and details)
4. Review (final review before submission)
## Guest vs Logged-In Checkout
**IMPORTANT:** Guest checkout availability depends on backend support.
**Guest checkout (recommended if backend supports it):**
- Reduces friction (no signup barrier)
- "Checkout as Guest" option prominent
- Email required for order confirmation
- Optional "Create account?" checkbox after order
- Don't force account creation
**Logged-in checkout:**
- Pre-fill saved addresses and payment methods
- "Returning customer? Log in" link at top
- Allow seamless switch between guest/login
## Component Architecture (RECOMMENDED)
**Build separate components for each checkout step for better maintainability and reusability.**
**Create individual step components:**
- `ShippingInformationStep` - Contact and shipping address form
- `DeliveryMethodStep` - Shipping method selection
- `PaymentInformationStep` - Payment method and details
- `OrderReviewStep` - Final review before submission
**Benefits of component separation:**
- **Maintainability**: Fix bugs or update one step without affecting others
- **Reusability**: Reuse shipping address component in account settings, checkout, etc.
- **Testability**: Test each step independently
- **Code organization**: Clearer separation of concerns (validation, submission logic per step)
- **Easier debugging**: Isolate issues to specific steps
- **Flexibility**: Easy to reorder steps or add/remove steps based on requirements
- **Performance**: Lazy load steps or split bundles for faster initial load
**What to separate:**
- Main checkout page/container component
- Individual step components (ShippingInformationStep, DeliveryMethodStep, etc.)
- Reusable order summary component (shown on all steps)
**Component communication:**
Each step component should accept:
- Current step data (form values)
- Callback to update data (e.g., `onShippingUpdate`)
- Callback to proceed to next step (e.g., `onContinue`)
- Loading/error states
- Validation errors
**Shared components:**
- Address form (used in shipping and billing)
- Payment method selector
- Order summary (sidebar, shown on all steps)
**Works for both single-page and multi-step:**
- Single-page: Render all steps at once, scroll-based navigation
- Multi-step: Show one component at a time, controlled by step state
- Accordion: Expand/collapse components as sections
**Common mistake:**
- ❌ Building entire checkout as one massive component with all form fields, logic, and validation mixed together
- ✅ Separate components for each step, shared state management in parent
## Checkout Flow
### Complete Checkout Flow Diagram
```
┌─────────────────────────────────────────────────────────────────────┐
│ CHECKOUT PROCESS │
└─────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ Optional: Guest Checkout vs Login │
│ • Guest: Enter email only │
│ • Logged-in: Pre-fill saved data │
└────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ STEP 1: Shipping Information │
│ ├─ Contact: Email, Phone │
│ ├─ Shipping Address: Name, Address, City, etc. │
│ └─ Billing Address: □ Same as shipping / Different │
└────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ STEP 2: Delivery │
│ • Fetch shipping methods from backend │
│ • Display: Standard, Express, Overnight │
│ • Show: Cost + Delivery estimate │
│ • Update order total │
└────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ STEP 3: Payment Information │
│ • Fetch payment methods from backend │
│ • Options: Card, PayPal, Apple Pay, etc. │
│ • Enter: Card details (tokenized) │
│ • Use billing address from Step 1 │
└────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ STEP 4: Order Review │
│ • Review: Items, addresses, shipping, payment │
│ • Optional: □ Agree to Terms and Conditions │
│ • Click: [Place Order] Button │
│ → Payment processing triggered │
└────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ Loading: Processing payment... │
│ • Authorize/capture payment via gateway │
│ • Create order in backend │
│ • Send confirmation email │
└────────────────────┬─────────────────────────────────┘
┌────┴────┐
│ │
Success Failure
│ │
▼ ▼
┌───────────────────┐ ┌──────────────────────┐
│ Order Confirmation│ │ Show Error Message │
│ • Order number │ │ • Retry payment │
│ • Details │ │ • Keep form data │
│ • Tracking link │ │ • Suggest solutions │
└───────────────────┘ └──────────────────────┘
```
## Key Ecommerce Considerations
### Shipping Address Collection
Collect:
- Required: email, name, address, city, state/zip, country
- Optional: phone.
**Key ecommerce considerations:**
- Email placement: First if guest checkout (identifies customer)
- Country placement: Early if shipping methods vary by country (affects available shipping)
- Phone: Optional to reduce friction, but recommended for delivery coordination
- Billing address: "Same as shipping" checkbox (default checked)
**For Medusa backends:**
- Country dropdown: Show only countries from cart's region (don't show all countries globally)
- Get countries from: `cart.region.countries` or `sdk.store.region.retrieve(cart.region_id)`
- Medusa regions contain specific countries - limiting options ensures correct pricing and shipping
- If user needs different country, they must change region first (typically via country selector component)
### Shipping Method Selection
**Fetch from backend after address provided** (shipping methods vary by address/region):
- Display as radio buttons with cost + delivery estimate
- Update order total immediately when method changes
- Highlight free shipping if available
- Show "Add $X for free shipping" if close to threshold
- Handle unavailable shipping: show message, suggest alternatives
### Payment Method Selection
**CRITICAL: Always fetch payment methods from backend and allow user to select from available options.**
Payment methods vary by store configuration (backend settings). NEVER assume which payment methods are available or hardcode payment options. Users MUST be able to choose their preferred payment method.
**Fetch available methods from backend:**
```typescript
// ALWAYS fetch payment providers from backend
// For Medusa:
const { payment_providers } = await sdk.store.payment.listPaymentProviders()
// For other backends:
// Change based on the integrated backend
const paymentMethods = await fetch(`${apiUrl}/payment-methods`)
// Returns: card, paypal, apple_pay, google_pay, stripe, etc.
```
**Display payment method selection UI:**
- Show all enabled payment providers returned by backend
- Allow user to select their preferred method (radio buttons or cards)
- Don't skip selection step - user must actively choose
- Map backend codes to display names in the storefront. For example `pp_system_manual` -> `Manual payment`.
- Common options: Credit/Debit Card, PayPal, Apple Pay, Google Pay, Buy Now Pay Later
**Available payment methods (examples, actual options come from backend):**
- Credit/Debit Card (most common, via Stripe/Braintree/other gateway)
- PayPal (redirect or in-context)
- Apple Pay (Safari, iOS only)
- Google Pay (Chrome, Android)
- Buy Now Pay Later (Affirm, Klarna - if enabled by store)
- Manual payment (bank transfer, cash on delivery - if enabled)
**Why backend fetching is required:**
- Store admin controls which payment providers are enabled
- Payment methods vary by region, currency, order value
- Test vs production mode affects available methods
- Can't assume all stores use the same payment gateway
**For Medusa backends - Payment flow:**
1. **List available payment providers:**
```typescript
const { payment_providers } = await sdk.store.payment.listPaymentProviders({
region_id: cart.region_id // Required to get region-specific providers
})
```
2. **Display providers and allow user to select:**
Show payment providers as radio buttons or cards. User must actively select one.
3. **Initialize payment session after selection:**
```typescript
// When user selects a provider
await sdk.store.payment.initiatePaymentSession(cart, {
provider_id: selectedProvider.id // e.g., "pp_stripe_stripe", "pp_system_default"
})
// Re-fetch cart to get updated payment session data
const { cart: updatedCart } = await sdk.store.cart.retrieve(cart.id)
```
4. **Render provider-specific UI:**
- Stripe providers (`pp_stripe_*`): Render Stripe Elements card UI
- Manual payment (`pp_system_default`): No additional UI needed
- Other providers: Implement according to provider requirements
**Important:** Payment provider IDs are returned from the backend (e.g., `pp_stripe_stripe`, `pp_system_manual`). Map these to user-friendly display names in your UI.
**Digital wallets (mobile priority):**
- Apple Pay / Google Pay should be prominent on mobile
- One-click payment (pre-filled shipping)
- Significantly faster checkout
- Higher conversion on mobile
**Card payment:**
- Use payment gateway (Stripe Elements, Braintree)
- Never handle raw card data (PCI compliance)
- Tokenize card data before submission
- Auto-detect card type (show logo)
### Order Review
**Display for final confirmation:**
Cart items, addresses, shipping method/cost, payment method, order total breakdown.
**Key elements:**
- "Edit" link next to each section (returns to step or edits inline)
- Terms checkbox (if required): "I agree to Terms and Conditions"
- Place Order button: Large (48-56px), shows total, loading state on submit
### Order Summary Sidebar
**Desktop:** Sticky sidebar with items, prices, totals. Updates in real-time.
**Mobile:** Collapsible at top ("Show order summary" toggle). Keeps focus on form.
## Backend Integration
**Address validation (optional):**
- Use address lookup APIs (Google, SmartyStreets) for higher accuracy
- Tradeoff: accuracy vs friction. Consider for high-value orders.
**Payment processing flow:**
1. Frontend tokenizes payment (Stripe Elements, Braintree)
2. Send token + order details to backend
3. Backend authorizes/captures payment & creates order
4. Redirect to confirmation page
**Never:** Send raw card data, store cards without PCI compliance, process payments client-side.
**On payment failure:** Show specific error, keep form data, allow retry without re-entering.
### Order Completion and Cart Cleanup (CRITICAL)
**After order is successfully placed, you MUST reset the cart state:**
**Common issue:** Cart popup and cart state still show old cart content after order is placed. This happens because the global cart state (Context, Zustand, Redux) isn't cleared after checkout completion.
**Required actions on successful order:**
1. **Clear cart from global state:**
- Reset cart state in Context/Zustand/Redux to null or empty
- Update cart count to 0 in navbar
- Prevent old cart items from showing in cart popup
2. **Clear localStorage cart ID:**
- Remove cart ID from localStorage: `localStorage.removeItem('cart_id')`
- Or create new cart and update cart ID in localStorage
- Ensures fresh cart for next shopping session
3. **Invalidate cart queries (if using TanStack Query):**
- `queryClient.invalidateQueries({ queryKey: ['cart'] })`
- Or `queryClient.removeQueries({ queryKey: ['cart', cartId] })`
- Prevents stale cart data from cache
4. **Redirect to order confirmation page:**
- Navigate to `/order-confirmation/[order_id]` or `/thank-you/[order_id]`
- Show order details, tracking info, confirmation
**Pattern:**
```typescript
// After successful order placement
async function onOrderSuccess(order) {
// 1. Clear cart state
setCart(null) // or clearCart() from context
// 2. Clear localStorage
localStorage.removeItem('cart_id')
// 3. Invalidate queries (if using TanStack Query)
queryClient.invalidateQueries({ queryKey: ['cart'] })
// 4. Redirect to confirmation
router.push(`/order-confirmation/${order.id}`)
}
```
**Why this is critical:**
- Without clearing cart state, cart popup shows old items after order
- User sees "phantom cart" if they click cart icon after checkout
- Creates confusion and poor UX
- May prevent user from starting new shopping session
## Mobile Checkout
**Key optimizations:**
- Digital wallets prominent (Apple Pay/Google Pay) - significantly faster checkout
- Single-column layout, 44-48px touch targets
- Appropriate keyboard types, autocomplete attributes enabled
- Collapsible order summary at top (shows total, expands on tap)
- Sticky Place Order button at bottom (always accessible, shows total)
- Accordion sections (one step at a time, reduces cognitive load)
**For detailed mobile patterns and safe area insets**, see `reference/mobile-responsiveness.md`.
## Trust and Conversion Optimization
**Trust signals (critical for conversion):**
- "Secure Checkout" badge, payment provider logos (Visa, Mastercard)
- Return policy link visible, customer support contact
- Near Place Order: "100% secure checkout", guarantees/free returns if offered
- For new brands: Customer review count, social proof
**Reduce abandonment:**
- Progress indicator (shows steps remaining)
- Auto-save form data, clear pricing (no surprise fees)
- Minimal required fields, smart defaults, autocomplete enabled
**Reduce perceived friction:**
- "No account required" (guest checkout)
- "Free shipping" highlighted
- Time estimate: "Less than 2 minutes"
## Error Handling
**Form validation:**
- Validate on blur, show error below field
- User-friendly messages ("Please enter a valid email address")
- Scroll to first error on submit
**Payment errors:**
- Card declined: "Your card was declined. Please try another payment method."
- Keep form data, suggest alternatives (try another card, PayPal)
- Network timeout: Show retry option without re-entering data
**Stock availability errors:**
- Out of stock: Remove item, recalculate, allow continue with remaining items
- Quantity reduced: Update automatically, show message, allow continue
## Checklist
**Essential checkout features:**
- [ ] **RECOMMENDED: Separate components created for each checkout step**
- [ ] Components: ShippingInformationStep, DeliveryMethodStep, PaymentInformationStep, OrderReviewStep
- [ ] Decision made: Single-page or multi-step (based on complexity)
- [ ] Guest checkout option (if backend supports it)
- [ ] Email field first (if guest checkout)
- [ ] Shipping address form with autocomplete attributes
- [ ] "Billing same as shipping" checkbox (default checked)
- [ ] Shipping methods fetched from backend dynamically
- [ ] Shipping cost updates order total in real-time
- [ ] **CRITICAL: Payment methods fetched from backend (NEVER assume or hardcode)**
- [ ] **CRITICAL: Payment method selection UI shown to user (user must select from available options)**
- [ ] Payment methods: show only enabled providers returned by backend
- [ ] For Medusa: Payment session initialized after user selects provider (sdk.store.payment.initiatePaymentSession)
- [ ] For Medusa: Country dropdown limited to cart's region countries
- [ ] Digital wallets prominent on mobile (Apple Pay, Google Pay)
- [ ] Payment tokenization (never send raw card data)
- [ ] Order review section before submission
- [ ] Order summary sidebar (sticky on desktop, collapsible on mobile)
- [ ] Promo code input (if not applied in cart)
- [ ] Trust signals throughout (secure checkout, return policy)
- [ ] Terms and conditions checkbox (if required)
- [ ] Place Order button prominent (48-56px, shows total)
- [ ] Loading state during payment processing
- [ ] Progress indicator (if multi-step)
- [ ] Clear error messages for validation failures
- [ ] Error handling for payment failures (keep form data)
- [ ] Stock availability check before order creation
- [ ] Mobile optimized (44-48px touch targets, single column)
- [ ] Autocomplete enabled on all form fields
- [ ] Keyboard accessible (tab through fields, enter to submit)
- [ ] ARIA labels on form fields (aria-required, aria-invalid)
- [ ] Redirect to order confirmation on success
- [ ] **CRITICAL: Clear cart state after successful order** (reset cart in Context/Zustand, remove cart ID from localStorage, invalidate cart queries)
- [ ] Cart popup shows empty cart after order completion (not old items)

View File

@@ -0,0 +1,264 @@
# Homepage Layout
## Contents
- [Overview](#overview)
- [Essential Homepage Sections](#essential-homepage-sections)
- [Hero Section](#hero-section)
- [Featured Categories](#featured-categories)
- [Product Sections](#product-sections)
- [Value Propositions](#value-propositions)
- [Newsletter Signup](#newsletter-signup)
- [Content Hierarchy](#content-hierarchy)
- [Mobile Considerations](#mobile-considerations)
- [Checklist](#checklist)
## Overview
The homepage is the primary landing page for an ecommerce store. Purpose: Strong first impression, guide users to products, communicate value, drive conversions.
**Backend Integration (CRITICAL):**
All content (categories, products, promotions) must be fetched from the ecommerce backend. Do this based on backend integrated. Never hardcode homepage content. Fetch featured products, bestsellers, new arrivals, and categories dynamically.
### Key Ecommerce Functions
- Create strong first impression (builds trust)
- Guide users to products they want (reduce bounce rate)
- Showcase featured products and promotions (increase conversion)
- Communicate value propositions (free shipping, returns)
- Capture email addresses (build marketing list)
- Drive conversions and sales
## Essential Homepage Sections
### Must-Have Sections
**Critical for every homepage:**
1. Hero section (above the fold)
2. Category navigation (product discovery)
3. Featured/bestselling products (social proof)
4. Footer (contact, legal, navigation)
**Strongly recommended:**
1. Value propositions (free shipping, returns, etc.)
2. Social proof (reviews, testimonials, trust badges)
3. New arrivals or sale section
4. Newsletter signup
### Content Hierarchy Decision
**Above the fold (first screen):**
- Hero section with main message
- Primary call-to-action
- Key value propositions (optional)
**Middle sections:**
- Featured categories
- Product sections (bestsellers, new arrivals, sale)
- Promotional banners
- Social proof
**Bottom sections:**
- Newsletter signup
- Footer
## Hero Section
Large banner at top of homepage, first thing users see (above the fold). Communicates main message or promotion.
**Content options:**
- Seasonal campaign or sale
- New product arrivals
- Brand message or value proposition
- Featured product category
- Multiple rotating slides (carousel) - max 3-4 slides
**See also:** [hero.md](../components/hero.md) for detailed hero section guidelines including carousel patterns, mobile optimization, and performance.
## Featured Categories
### Purpose and Display
**Purpose**: Help users browse by category, reduce clicks to reach products, quick access to main product types.
**Category selection:**
- 4-8 main categories
- Most popular or seasonal categories
- Balanced representation
- **Fetch from backend dynamically** (never hardcode)
### Display Patterns
**Pattern 1: Category Grid with Images**
3-6 category tiles with images, category name overlay, click to navigate. Equal-sized tiles in grid layout (3-4 columns desktop, 2 mobile).
**Pattern 2: Category Cards**
Card layout with category image, name, and item count ("120 products"). "Shop [Category]" button on each card. 2-4 columns on desktop, 2 on mobile.
**Pattern 3: Category Slider**
Horizontal scrollable categories showing 4-6 at once. Arrows for navigation. See [product-slider.md](../components/product-slider.md).
## Product Sections
### Bestsellers Section (CRITICAL)
**Purpose**: Showcase popular products, build social proof, guide uncertain shoppers.
**Product selection (backend-driven):**
- Sort by total sales volume
- Update regularly (weekly or monthly)
- Mix of categories (not all one type)
- Show 8-15 products
**Layout:**
Product slider or grid, product cards with image, title, price, rating (if available). "View All" link to full bestsellers page.
### New Arrivals Section
**Purpose**: Highlight latest products, create sense of freshness, encourage repeat visits.
**Product selection:**
- Most recently added products (last 30 days)
- Sorted by newest first
- Exclude out-of-stock items
- Show 10-20 products
**Layout:**
Product slider, optional "New" badge on products. "Shop New Arrivals" link.
### Sale/Promotional Products
**Purpose**: Drive urgency and conversions, clear excess inventory.
**Product selection:**
- Products with active sale prices from backend
- Sorted by discount percentage
- Limited time or seasonal sales
**Display:**
Product slider with sale badges, strikethrough pricing, optional countdown timer (if time-limited sale).
## Value Propositions
### Trust & Convenience Features
**Purpose**: Build trust quickly, address common concerns (shipping cost, returns), differentiate from competitors.
**Common value propositions:**
- Free shipping (over threshold or always)
- Free returns (30/60/90 days)
- Secure checkout
- Customer support (24/7, phone, chat)
- Fast shipping (2-day, same-day)
- Quality guarantee or warranty
### Display Pattern
**Icon Row (most common):**
3-4 icons with text below hero section. Icon + short text (truck icon + "Free Shipping"). Single row, centered. 100-150px per item.
**Placement:**
Below hero section (most common) or above footer.
## Newsletter Signup
### Email Capture Section
**⚠️ IMPORTANT: Check footer first - don't duplicate newsletter forms.**
If your footer already includes a newsletter signup form, **do NOT add another newsletter section on the homepage**. Two newsletter forms on the same page:
- Creates confusion (which one to use?)
- Looks unprofessional and repetitive
- Reduces conversion (decision fatigue)
- Wastes valuable homepage space
**Decision:**
- Footer has newsletter? → Skip homepage newsletter section, use that space for other content
- Footer doesn't have newsletter? → Add homepage newsletter section (recommended placement: mid-page)
- Want both? → Only if they serve different purposes (e.g., footer = general newsletter, homepage = specific campaign/offer)
**Purpose**: Grow email list for marketing, offer incentive to build relationship.
**Design essentials:**
- Heading: "Stay in the Loop", "Get 10% Off"
- Subheading: Benefit of subscribing (don't just say "subscribe")
- Email input field
- Submit button
- Privacy note (optional): "We respect your privacy"
**Incentive (CRITICAL):**
- 10% off first order (most common)
- Early access to sales
- Exclusive content or products
- Free shipping code
**Layout options:**
- Full-width section (dedicated section, background color, centered)
- Inline form (between sections, smaller)
- Footer newsletter (part of footer) - see footer.md
**Placement:**
Mid-page (after 2-3 sections) or above footer.
## Content Hierarchy
### Section Order Recommendation
**Typical homepage structure:**
1. Hero section
2. Value propositions (free shipping, returns)
3. Featured categories
4. Bestsellers or Featured Products
5. Promotional banner (mid-page, optional)
6. New Arrivals
7. Newsletter signup (skip if footer already has newsletter form)
8. Footer
### Visual Rhythm
**Vary section types:**
Product section → Banner → Product section. Avoid monotony (all product sections in a row). Mix text-heavy and image-heavy sections.
**Spacing:**
Generous padding between sections (64-120px desktop, 40-60px mobile). Consistent spacing. Section backgrounds to create separation.
## Mobile Considerations
**Responsive layout:**
Single column for most sections, stack elements vertically, larger touch targets (44px minimum), simplified navigation.
**Product sections:**
Horizontal sliders with swipe gestures, or stacked product grids (2 columns). Smaller product cards optimized for mobile.
**Hero section:**
Portrait aspect ratio (2:3 or 3:4), vertical text placement (center/bottom). See [hero.md](../components/hero.md) for mobile hero details.
**Performance:**
Lazy load below-fold images, optimize hero image (<200KB), use WebP format. Mobile-first approach.
## Checklist
**Essential elements:**
- [ ] Hero section with clear message and CTA
- [ ] Featured categories (4-8 categories with images)
- [ ] Categories fetched from backend dynamically
- [ ] Bestsellers or Featured Products section
- [ ] New Arrivals section
- [ ] Value propositions (free shipping, returns, etc.)
- [ ] Social proof (reviews, ratings, testimonials)
- [ ] Newsletter signup form (only if footer doesn't have one - check footer first)
- [ ] No duplicate newsletter forms (homepage + footer)
- [ ] Footer with navigation and legal links
- [ ] Mobile-responsive layout (single column, 44px touch targets)
- [ ] Fast loading time (<3 seconds)
- [ ] Optimized images (<200KB, WebP format)
- [ ] Lazy loading for below-fold content
- [ ] Backend integration (all content fetched from API)
- [ ] Semantic HTML (main, section, h1, h2)
- [ ] Proper heading hierarchy (h1 → h2 → h3)
- [ ] Keyboard accessible
- [ ] ARIA labels on sections
- [ ] High contrast text (4.5:1 minimum)
- [ ] Clear CTAs on every section

View File

@@ -0,0 +1,231 @@
# Order Confirmation Page
## Contents
- [Overview](#overview)
- [Essential Information](#essential-information)
- [Order Details Display](#order-details-display)
- [Next Steps Section](#next-steps-section)
- [Layout Patterns](#layout-patterns)
- [Call-to-Action Buttons](#call-to-action-buttons)
- [Mobile Considerations](#mobile-considerations)
- [Checklist](#checklist)
## Overview
The order confirmation page displays immediately after successful checkout. Purpose: Confirm purchase, provide order details, guide customers on next steps, build post-purchase confidence.
**Backend Integration:**
Fetch order details from backend API immediately after checkout completes. Do this based on backend integrated. Never hardcode or mock order data.
### Key Ecommerce Functions
- Confirm successful purchase (reduces anxiety)
- Provide order reference number (for tracking and support)
- Set delivery expectations (reduces "where's my order" inquiries)
- Guide customer on next steps (reduces support load)
- Encourage repeat purchases (continue shopping CTA)
- Build post-purchase confidence (reduces buyer's remorse)
## Essential Information
### Success Message (CRITICAL)
**Confirmation headline:**
Large, prominent heading, positive reassuring message, success icon (green checkmark), immediately visible above fold.
**Example messages:**
- "Order Confirmed!"
- "Thank you for your order!"
- "Success! Your order is confirmed"
**Subheading:**
Brief reassurance, email confirmation mention, delivery timeframe preview.
**Example:**
```
✓ Order Confirmed!
Thank you for your purchase! We've sent a confirmation
email to customer@example.com with your order details.
```
### Order Number
**Display requirements:**
- Very prominent
- Clearly labeled "Order Number:" or "Order #"
- Easy to select and copy (selectable text)
- Monospace or sans-serif font
- High contrast for visibility
- Optional: Copy button next to number
**Example:**
```
Order Number: #ORD-123456789
```
### Email Confirmation Notice
Confirmation email was sent, email address used, check spam folder reminder (optional), resend email option (optional).
## Order Details Display
### Ordered Items List
Display per item:
- Product image (thumbnail, 60-80px)
- Product title (full name)
- Variant information (size, color, etc.)
- Quantity ("× 2")
- Unit price
- Line total (quantity × price)
### Order Summary (Pricing)
**Price breakdown:**
- Subtotal (sum of items)
- Shipping cost (with method name)
- Tax (if applicable)
- Discount/promo code (if used, show savings)
- **Order Total** (bold, larger font)
**Medusa pricing note:**
Medusa stores prices as-is (not in cents). Display prices directly without dividing by 100. Example: If backend returns 49.99, display $49.99.
**Example:**
```
Subtotal: $139.97
Shipping (Express): $5.99
Tax: $11.20
Discount (SAVE10): -$14.00
─────────────────────────────
Order Total: $143.16
```
### Shipping and Billing Information
**Shipping address:**
Recipient name, complete address, phone number, shipping method selected, estimated delivery date.
**Billing address:**
If same as shipping: "Same as shipping address". If different: Show full billing address.
**Payment information:**
Payment method type, last 4 digits (if card), alternative methods (PayPal email, Apple Pay). **Never show full card number.**
## Next Steps Section
### What Happens Next (CRITICAL)
**Timeline guidance:**
Order processing information, shipment timeline, when tracking becomes available, expected delivery date.
**Example:**
```
What's Next?
1. Order Processing (1-2 business days)
We're carefully preparing your items for shipment.
2. Shipment Notification
You'll receive an email with tracking information
when your order ships.
3. Delivery (By January 30)
Your package will arrive at your address.
```
**Benefits:**
Sets clear expectations, reduces "where's my order" inquiries, builds confidence in process.
## Layout Patterns
### Single Column Layout (Recommended)
Full-width content, centered on page, all sections stacked vertically. Mobile-friendly by default.
**Section order:**
1. Success message and order number
2. Email confirmation notice
3. Order items list
4. Order summary (pricing)
5. Shipping address
6. Billing address
7. Payment method
8. Next steps/timeline
9. CTAs (continue shopping, print, track)
### Two-Column Layout (Desktop Alternative)
- Left column (60-70%): Main content (success, order number, items, addresses, next steps)
- Right column (30-40%): Sidebar (order summary, CTAs, tracking)
- Mobile: Collapses to single column
## Call-to-Action Buttons
### Primary Actions
**Continue Shopping (MOST IMPORTANT):**
Large, prominent button (primary color), returns to homepage or shop page. Text: "Continue Shopping" or "Back to Store". Encourages repeat visits.
### Secondary Actions
**Create Account (for guest orders):**
Encourage account creation, pre-fill email from order, benefits messaging ("Track orders easily"). Optional, not required.
**Print Receipt:**
Print-friendly CSS, button to print page.
**Contact Support:**
Link to support page or contact form, phone number (if available), help with order questions.
**Button layout:**
Primary action prominent (filled button), secondary actions less prominent (outline or link), adequate spacing (16-24px), mobile full-width.
## Mobile Considerations
**Single column only:**
Full-width sections, generous padding (16-20px), larger text for important info, touch-friendly buttons.
**Order number:**
Extra large (28-36px), highly visible, easy to read and reference, tap to copy (if implemented).
**Buttons:**
Full-width or near full-width (min 90%), 48-56px height (touch-friendly), 16px spacing between buttons.
**Quick actions:**
- Tap phone number to call support
- Tap to copy order number
- Add delivery date to calendar
- Share order details
## Checklist
**Essential elements:**
- [ ] Large success message (32-48px heading)
- [ ] Green checkmark or success icon
- [ ] Prominent order number (24-32px, selectable)
- [ ] Email confirmation notice
- [ ] Order items list with images
- [ ] Item details (title, variant, quantity, price)
- [ ] Order summary (subtotal, shipping, tax, total)
- [ ] Shipping address displayed
- [ ] Billing address (or "same as shipping")
- [ ] Payment method (last 4 digits only)
- [ ] Estimated delivery date
- [ ] Shipping method name
- [ ] "What's Next" section (timeline)
- [ ] Continue Shopping button (primary CTA)
- [ ] Print receipt button
- [ ] Contact support link
- [ ] Guest orders: Create account CTA (optional)
- [ ] Backend integration (fetch order from API)
- [ ] Mobile-responsive (single column, full-width buttons)
- [ ] Semantic HTML (main, section, h1, h2)
- [ ] ARIA labels on sections
- [ ] Live region announcing success
- [ ] Keyboard navigation supported
- [ ] High contrast text (4.5:1 minimum)

View File

@@ -0,0 +1,527 @@
# Product Detail Page Layout
## Contents
- [Overview](#overview)
- [Layout Structure](#layout-structure)
- [Price Display and Medusa Integration](#price-display-and-medusa-integration)
- [Variant Selection (Critical)](#variant-selection-critical)
- [Stock Availability](#stock-availability)
- [Add to Cart Behavior](#add-to-cart-behavior)
- [Product Details Organization](#product-details-organization)
- [Related Products Strategy](#related-products-strategy)
- [Trust Signals and Conversion](#trust-signals-and-conversion)
- [Mobile Optimization](#mobile-optimization)
- [Checklist](#checklist)
## Overview
Most critical page for conversion. Customers make purchase decisions here based on product information, images, reviews, and trust signals.
### Key Requirements
- High-quality product images with zoom capability
- Clear price display (handle variant price changes)
- Variant selection (size, color, material)
- Stock availability indicators
- Prominent "Add to Cart" with proper feedback
- Product details (description, specifications)
- Customer reviews and ratings
- Related product recommendations
- Trust signals (shipping, returns, secure checkout)
- Mobile-optimized (60%+ traffic)
### Routing Pattern
**CRITICAL: Always use dynamic routes, NEVER static pages.**
Product detail pages must use dynamic routes that accept a parameter (handle, slug, or ID):
**Correct examples:**
- Next.js App Router: `app/products/[handle]/page.tsx`
- Next.js Pages Router: `pages/products/[handle].tsx`
- SvelteKit: `routes/products/[handle]/+page.svelte`
- TanStack Start: `routes/products/$handle.tsx`
- Remix: `routes/products.$handle.tsx`
**Wrong examples:**
-`pages/products/blue-shirt.tsx` (static file per product)
-`pages/products/red-shoes.tsx` (doesn't scale)
Fetch product data in the dynamic route based on the handle/ID parameter from the URL.
## Layout Structure
**Desktop (two-column):**
- Left: Product images (50-60% width)
- Right: Product info, variants, add to cart (40-50%)
- Below: Product details, reviews, related products (full-width)
**Mobile (stacked):**
- Images at top (full-width, swipeable)
- Product info below (title, price, rating)
- Variants and add to cart
- Accordion for product details
- Reviews section
- Related products
- Sticky "Add to Cart" bar at bottom
**Sticky sidebar option (desktop):**
- Product info column stays visible during scroll
- Add to cart always accessible
- Useful for long product descriptions
- Improves conversion
## Price Display
### Standard Price Display
**Current price:**
- Large, bold font (28-36px)
- Currency symbol included ($49.99)
- Primary color or black
**Sale pricing:**
- Original price with strikethrough: ~~$79.99~~ $49.99
- Sale price in red or brand color
- "Save X%" badge nearby
- Example: Save 37%
**Variant price changes:**
- **When no variant selected**: Show "From $X" where X is the minimum variant price across all variants
- **When variant selected**: Update price dynamically to show the exact variant price
- No page reload required
- Show price change clearly (highlight briefly on change)
- Example: Product with variants priced at $29.99, $34.99, $39.99 → Show "From $29.99" initially
### Medusa Pricing (CRITICAL)
**Important difference from Stripe:**
- Medusa stores prices as-is (e.g., 49.99)
- Display directly: If API returns 49.99, show $49.99
- **DON'T divide by 100** (unlike Stripe which stores in cents)
- Example: Medusa 49.99 → Display $49.99 (NOT $0.4999)
**Multi-currency (Medusa):**
- Medusa supports multi-region pricing
- Display price in user's region currency
- Fetch pricing from selected region
- Show currency code (usd, eur, etc.)
## Variant Selection (Critical)
This is a complex ecommerce-specific challenge. Variants affect price, stock, and images.
### Variant Complexity
**Key challenges:**
- Multiple variant types (size, color, material)
- Variant availability varies (some sizes out of stock)
- Prices may differ by variant
- Images change by color variant
- Stock levels per variant
- Combinations may not exist (size M + color Red might not exist)
**Fetch from backend:**
```typescript
// Get all variants for product
// Change this based on the backend integrated
const product = await fetch(`/products/${id}?fields=*variants`)
// Returns variants with: id, sku, options, calculated_price, inventory_quantity
```
### Variant Selection Patterns
**Use Button Group when:**
- 2-8 options per variant type
- Size selection (XS, S, M, L, XL)
- Simple color options (5-6 colors)
- Users need to see all options at once
**Benefits:**
- Visible options (no click to reveal)
- Faster selection
- Clear visual feedback
- Better UX
**Use Dropdown when:**
- 10+ options per variant type
- Material/style options with long names
- Space-constrained layouts
- Mobile optimization needed
**Benefits:**
- Saves space
- Works better for many options
- Mobile-friendly
**Use Visual Swatches when:**
- Color or pattern variations
- Material with visual differences
- Visual is key to decision
- Fashion, home decor, customizable products
**Implementation:**
- Circular/square swatches (40-48px)
- Border on selected
- Show product image in that color when selected
- Color name on hover
- Gray out unavailable colors
### Variant Selection Flow
**Critical sequence:**
1. User selects first variant type (e.g., Color: Blue)
2. **Update available options** for other variant types
3. Show only size options available for Blue color
4. Gray out/disable unavailable combinations
5. Update price if variant price differs
6. Update main product image to show selected variant
7. Update stock availability
8. Enable/disable "Add to Cart" based on availability
**Example: Two variants (Color + Size)**
```typescript
// When color selected
// Change this based on the backend integrated
onColorSelect(color) {
// Find selected variant
const selectededVariant = product.variants.find((variant) => variant.options?.every(
(optionValue) => optionValue.id === selectedOptions[optionValue.option_id!]
))
// Check if size is selected and update price
if (selectededVariant) {
const variant = findVariant(color, selectedSize)
updatePrice(variant.price)
updateStock(variant.inventory_quantity)
}
}
```
### Validation and Error Handling
**Prevent adding without selection:**
- Disable "Add to Cart" until all required variants selected
- Or: Show error message "Please select a size"
- Highlight missing selection (red border around options)
- Scroll to variant selection on error
**Handle out of stock variants:**
- Gray out unavailable options
- "Out of stock" text on hover
- Don't allow selection of out of stock variants
- Suggest alternative variants if available
**Handle variant not found:**
- When combination doesn't exist (Size M + Color Red)
- Disable second option when first selected
- Show only valid combinations
- Or: Show "This combination is not available"
## Stock Availability
**Display patterns:**
**In stock:**
- Green indicator (✓ or dot)
- "In stock" or "Available"
- Quantity if low: "Only 3 left"
- Encourages urgency without being pushy
**Out of stock:**
- Red indicator (✗ or dot)
- "Out of stock" message
- Disable "Add to Cart" button (grayed out)
- Offer "Notify me when available"
- Email capture for restock notifications (if supported by backend)
**Low stock warning:**
- "Only X left in stock"
- Shows scarcity (increases urgency)
- Typically show when <= 5 items
- Orange/yellow color
**Pre-order:**
- "Pre-order now" status
- Expected availability date: "Ships on [Date]"
- Different button text: "Pre-order" instead of "Add to Cart"
- Charge now or later (specify)
**Backend integration:**
```typescript
// Fetch stock for selected variant
const stock = selectedVariant.inventory_quantity
if (stock === 0) {
showOutOfStock()
} else if (stock <= 5) {
showLowStock(stock) // "Only 3 left"
} else {
showInStock()
}
```
## Add to Cart Behavior
**Button states:**
- Default: Enabled (after variant selected)
- Hover: Slight color change or scale
- Loading: Spinner inside button (during API call)
- Success: Checkmark briefly, then revert
- Disabled: Grayed out (no variant or out of stock)
**Click behavior (Critical):**
1. Show loading state (disable button, show spinner)
2. Call API to add item to cart (backend)
3. **Optimistic UI**: Update cart count immediately (before API response)
4. Show success feedback (toast, checkmark, or cart popup)
5. Update cart count in navbar header
6. **DON'T navigate away** - stay on product page
7. Handle errors: restore count if API fails
**Success feedback options:**
- Toast notification: "Added to cart" (top-right)
- Cart popup: Show mini cart with items (see cart-popup.md)
- Checkmark in button briefly, then revert
- All three combined (checkmark + toast or cart popup)
**Error handling:**
```typescript
async function addToCart(variantId, quantity) {
try {
// Optimistic update
updateCartCountUI(+quantity)
// API call
// Change this based on the backend integrated
await fetch(`/store/carts/${cartId}/line-items`, {
method: 'POST',
body: JSON.stringify({ variant_id: variantId, quantity })
})
// Success feedback
showToast('Added to cart')
showCartPopup() // Optional
} catch (error) {
// Revert optimistic update
updateCartCountUI(-quantity)
// Show error
if (error.message === 'OUT_OF_STOCK') {
showError('Sorry, this item is now out of stock')
updateStockStatus('out_of_stock')
} else {
showError('Failed to add to cart. Please try again.')
}
}
}
```
**Buy Now button (optional):**
- Skip cart, go directly to checkout
- Useful for: high-value items, single-item stores, decisive customers
- Secondary button below "Add to Cart"
- Text: "Buy Now" or "Buy It Now"
- Add to cart + redirect to checkout in one action
## Product Details Organization
### Decision: Tabs vs Accordion
**Use Tabs (desktop) when:**
- 3-5 distinct sections
- Each section has substantial content
- Users may want to compare sections
- Desktop has screen space
- Examples: Description, Specifications, Shipping, Reviews
**Use Accordion (mobile) always:**
- Saves vertical space
- Users expand what they need
- Standard mobile pattern
- Collapses after reading
**Hybrid approach (recommended):**
- Tabs on desktop (horizontal navigation)
- Accordion on mobile (vertical expansion)
- Same content, different presentation
- Best of both worlds
### Common Sections
**Description:**
- Product overview (2-4 paragraphs)
- Key features (bullet points)
- Use cases
- Materials and craftsmanship
**Specifications:**
- Technical details (table format)
- Dimensions, weight, materials
- Care instructions
- Compatibility information
**Shipping & Returns:**
- Shipping options and costs
- Delivery timeframes
- Return policy (30 days, 60 days)
- Return process
- Link to full policy page
**Reviews:**
- Embedded in tab/accordion
- Or: Separate section below
- Filter by rating, sort by date
- Review submission form
## Related Products Strategy
**Types of recommendations:**
**"You May Also Like" (Similar products):**
- Same category, similar price point
- Algorithm: category match + price range
- Goal: Show alternatives if unsure about current product
**"Frequently Bought Together" (Complementary):**
- Products commonly purchased together
- Algorithm: order history analysis
- Goal: Increase average order value
- Example: Phone + Case + Screen Protector
- Show bundle discount if available
**"Recently Viewed" (Browsing history):**
- User's browsing history (session or logged-in)
- Helps users return to products they liked
- Goal: Reduce decision paralysis
**"Customers Also Viewed":**
- Products viewed by others who viewed this
- Algorithm: co-viewing patterns
- Goal: Discovery and alternatives
### Display Pattern
**Product slider:**
- 4-6 products visible (desktop)
- 2-3 visible (mobile)
- Horizontal scrolling (swipe on mobile)
- Product cards: image, title, price, rating
- Optional: Quick "Add to Cart" on hover
**Placement:**
- Below product details and reviews
- Above footer
- Full-width section
- Clear heading for each type
**Backend integration:**
```typescript
// Fetch recommendations
// Change this based on the backend integrated
const recommendations = await fetch(`/products/${id}/recommendations`)
// Returns: similar, bought_together, recently_viewed
```
## Trust Signals and Conversion
**Essential trust signals:**
**Near Add to Cart:**
- Free shipping badge (if applicable)
- Free returns icon + text
- Secure checkout icon
- Money-back guarantee
- Warranty information (if applicable)
**Below product title:**
- Customer rating and review count (4.8 ★ 324 reviews)
- Link to reviews section
- "Best seller" or "Top rated" badge
**Payment methods:**
- Accepted payment icons (Visa, Mastercard, PayPal, Apple Pay)
- Small icons (40px)
- Below "Add to Cart" or in footer
- Shows payment options available
**For new/unknown brands:**
- Customer testimonials
- "Join 10,000+ happy customers"
- Security badges (if legitimate - don't fake)
- Social proof (Instagram photos, user content)
- Clear contact information
**For high-value products:**
- Detailed specifications
- Professional photography
- Video demonstrations
- Warranty details prominently displayed
- Customer service contact visible
## Mobile Optimization
**Critical mobile patterns:**
**Sticky "Add to Cart" bar:**
- Fixed at bottom of screen
- Always accessible (no scrolling needed)
- Shows: Price + "Add to Cart" button
- Appears after scrolling past fold
- Higher conversion rates
**Image gallery:**
- Full-width swipeable carousel
- Pinch to zoom
- Dot indicators (1/5, 2/5)
- Tap to open full-screen view
**Variant selection:**
- Large touch targets (44-48px)
- Visual swatches easier than dropdowns
- Clear selected state
- Error messages visible
**Accordion for details:**
- Description, Specs, Shipping as accordion
- Starts collapsed (save space)
- User expands what they need
- Clear expand/collapse indicators
**Reviews section:**
- Expandable (start with 2-3 reviews)
- "Show more" button
- Filter by rating
- Star rating distribution chart
## Checklist
**Essential product detail page features:**
- [ ] High-quality product images with zoom
- [ ] Price displayed correctly (Medusa: use value as-is, not divided)
- [ ] Price shows "From $X" when no variant selected (X = minimum variant price)
- [ ] Variant selection required before adding to cart
- [ ] Variant selection updates: price, stock, image
- [ ] Disable unavailable variant options (gray out)
- [ ] Stock availability indicator (in stock, low stock, out of stock)
- [ ] "Only X left" shown when stock is low (<=5)
- [ ] Add to Cart disabled until variant selected
- [ ] Optimistic UI update (cart count updates immediately)
- [ ] Success feedback (toast, cart popup, or checkmark)
- [ ] Stay on product page after adding (don't navigate away)
- [ ] Error handling (out of stock, API failure)
- [ ] Product description and specifications
- [ ] Customer reviews and ratings
- [ ] Related products recommendations (similar, bought together)
- [ ] Trust signals (free shipping, returns, secure checkout)
- [ ] Payment method icons displayed
- [ ] Breadcrumb navigation
- [ ] Mobile: Swipeable image gallery
- [ ] Mobile: Accordion for product details
- [ ] Mobile: Sticky Add to Cart bar (optional but effective)
- [ ] Tabs on desktop, accordion on mobile (hybrid)
- [ ] Fast loading (<2s, optimize images)
- [ ] Keyboard accessible (tab through options, enter to add)
- [ ] ARIA labels on variant selection (role="group", aria-label)

View File

@@ -0,0 +1,520 @@
# Product Listing Page Layout
## Contents
- [Overview](#overview)
- [Reusable Component Architecture](#reusable-component-architecture-recommended)
- [Decision: Pagination vs Infinite Scroll vs Load More](#decision-pagination-vs-infinite-scroll-vs-load-more)
- [Decision: Filter Pattern Selection](#decision-filter-pattern-selection)
- [Product Grid Layout](#product-grid-layout)
- [Filtering Strategy](#filtering-strategy)
- [Sorting Strategy](#sorting-strategy)
- [Backend Integration](#backend-integration)
- [Empty and No Results States](#empty-and-no-results-states)
- [Performance Optimization](#performance-optimization)
- [Mobile Optimization](#mobile-optimization)
- [Checklist](#checklist)
## Overview
Primary browsing interface where users compare products, apply filters, and navigate to product details. Critical for product discovery and conversion.
### Key Requirements
- Responsive product grid (3-4 columns desktop, 2 mobile)
- Filtering (categories, price, attributes)
- Sorting options (price, popularity, newest)
- Pagination, infinite scroll, or load more
- Results count and active filter indicators
- Clear "no results" state with suggestions
- Fast loading and filtering (<1s filter updates)
- Backend integration for dynamic filtering
### Reusable Component Architecture (RECOMMENDED)
**Build product listing as a reusable component that works across multiple pages:**
**Use the same product listing component for:**
- "Shop All" page (all products, no category filter)
- Category pages (filtered by specific category)
- Search results page (filtered by search query)
- Sale/Promotion pages (filtered by discount/promotion)
- Collection pages (curated product sets)
- Brand pages (filtered by brand)
**Benefits of reusable approach:**
- Single source of truth for product browsing UI
- Consistent filtering, sorting, and pagination behavior across entire site
- Easier maintenance (fix bugs once, applies everywhere)
- Better user experience (familiar interface on every product browsing page)
- Significantly less code duplication
**What to make configurable:**
- Initial filter parameters (category ID, search query, promotion ID, brand, etc.)
- Page title and breadcrumbs
- Whether to show filters sidebar (some pages may hide certain filters)
- Default sort order (category: featured, search: relevance, sale: discount %)
- Number of products per page
- Filter options available (hide category filter on category pages, etc.)
**Common mistake:**
- ❌ Creating separate components/pages for "Shop All", category pages, and search results with duplicated filtering/sorting/pagination logic
- ✅ Build one reusable ProductListing component that accepts filter parameters and reuse it across all product browsing pages
### Routing Pattern
**CRITICAL: Always use dynamic routes for category pages, NEVER static pages.**
Category/listing pages must use dynamic routes that accept a parameter (handle, slug, or category ID):
**Correct examples:**
- Next.js App Router: `app/categories/[handle]/page.tsx`
- Next.js Pages Router: `pages/categories/[handle].tsx`
- SvelteKit: `routes/categories/[handle]/+page.svelte`
- TanStack Start: `routes/categories/$handle.tsx`
- Remix: `routes/categories.$handle.tsx`
**Wrong examples:**
-`pages/categories/women.tsx` (static file per category)
-`pages/categories/men.tsx` (doesn't scale)
Fetch category products in the dynamic route based on the handle/ID parameter from the URL.
## Decision: Pagination vs Infinite Scroll vs Load More
This is a critical ecommerce decision that affects user experience, SEO, and technical implementation.
### Use Pagination When:
**User needs:**
- Return to specific result pages
- Precise control over browsing
- Professional/research shopping (compare systematically)
- B2B shoppers (procurement, large orders)
**Product characteristics:**
- Position matters (rankings, bestsellers)
- Large catalog with stable ordering
- Products require careful comparison
**Technical benefits:**
- SEO-friendly (unique URL per page)
- Better for indexing and crawling
- Easier back button support
- Lower memory usage
**Implementation:**
```typescript
// URL structure: /products?page=2&category=shirts
// Each page has unique URL for SEO
```
**Best for:**
- Desktop-heavy audience
- B2B ecommerce
- Product comparison shopping
- Catalog with 100+ products
### Use Infinite Scroll When:
**User needs:**
- Exploratory browsing behavior
- Mobile-first experience
- Seamless discovery flow
- Fashion/visual shopping
**Product characteristics:**
- Visual-heavy products (fashion, art, photography)
- Impulse purchases
- Discovery-focused (Pinterest-style)
**Technical considerations:**
- More complex to implement
- Requires careful SEO handling (pagination URLs still needed)
- Higher memory usage (all loaded products stay in DOM)
- Need to handle browser back button carefully
**Implementation:**
```typescript
// Load more when user scrolls to bottom
// Keep pagination in URL for SEO: /products?page=2
// Use Intersection Observer API for detection
```
**Best for:**
- Mobile-first stores (>60% mobile traffic)
- Fashion, home decor, visual products
- Younger demographic (18-34)
- Discovery-focused shopping
### Use "Load More" Button When:
**Benefits of compromise:**
- User controls when to load (not automatic)
- Footer remains accessible (important for policies, contact)
- Better for slower connections (international users)
- Accessibility friendly (no automatic loading)
- Lower memory usage than infinite scroll
**Implementation:**
```typescript
// Button triggers next page load
// Append products to existing grid
// Show count: "Load 24 More Products"
```
**Best for:**
- International audience (varying connection speeds)
- Footer content is important (legal, policies, contact)
- Accessibility concerns with infinite scroll
- Compromise between pagination and infinite scroll
### Hybrid Approach (Recommended):
Combine patterns based on context:
- Pagination for SEO (canonical URLs)
- Infinite scroll for UX (on user interaction)
- Load more for control (user-triggered)
**Example:**
```typescript
// Desktop: Pagination at bottom + infinite scroll option
// Mobile: Infinite scroll with pagination URLs for SEO
// All: Preserve scroll position on back button
```
## Decision: Filter Pattern Selection
### Sidebar Filters (Desktop)
**Use when:**
- Many filter options (5+ categories)
- Complex product attributes
- Power users (B2B, professional shoppers)
- Desktop-heavy traffic
**Layout:**
- Left sidebar (250-320px wide)
- Sticky position (scrolls with page)
- Collapsible sections (accordion)
- Apply immediately (no "Apply" button)
### Top Filters (Desktop)
**Use when:**
- Few filter options (2-4 key filters)
- Maximize grid space (full-width layout)
- Simple product categories
- Visual-first products (fashion)
**Layout:**
- Horizontal filter bar above grid
- Dropdowns or button toggles
- Limited options (price, category, brand)
- Compact design
### Drawer Filters (Mobile - Always)
**Pattern:**
- "Filters" button at top (shows active count badge)
- Slide-out drawer (full-screen or 80% width)
- Accordion sections
- "Apply" button at bottom (batch filtering)
- "Clear All" option
**Why batch filtering on mobile:**
- Prevents multiple re-renders on slow connections
- User can adjust multiple filters before applying
- Better mobile UX (less disruptive)
## Product Grid Layout
**Responsive columns:**
- Large desktop (>1440px): 4 columns
- Desktop (1024-1440px): 3-4 columns
- Tablet (768-1024px): 3 columns
- Mobile (< 768px): 2 columns
**Adjust based on product type:**
- Fashion/lifestyle: 3-4 columns (more visible at once)
- Electronics/detailed: 2-3 columns (larger cards, more detail)
- Furniture/large items: 2-3 columns (showcase details)
**Product card essentials:**
- Product image (primary)
- Title (truncated to 2 lines)
- Price (Medusa: display as-is, don't divide by 100)
- Optional: Rating, badges, wishlist
- See product-card.md for detailed guidelines
**Grid spacing:**
- 16-24px gap (desktop)
- 12-16px gap (mobile)
- Equal height rows (optional, improves visual consistency)
## Filtering Strategy
### Filter Types by Purpose
**Category filters:**
- Multi-select checkboxes
- Hierarchical (parent-child categories)
- Show product count per category
- Example: "Shirts (24)" "T-Shirts (12)"
**Price range filter:**
- Range slider (drag min/max)
- Or: Predefined ranges ("$0-$50", "$50-$100")
- Update dynamically as products filtered
- Show min/max from current results
**Attribute filters (Size, Color, Brand):**
- Multi-select checkboxes
- Visual swatches for colors
- Show available options based on current filters
- Gray out unavailable combinations
**Availability filters:**
- "In Stock" checkbox
- "On Sale" checkbox
- "New Arrivals" checkbox
- Single purpose, clear value
### Filter Behavior
**Filter persistence:**
- Save in URL parameters (shareable, bookmarkable)
- Example: `/products?category=shirts&price=0-50&color=blue`
- Restore filters on page reload
- Clear all filters should reset URL
### Active Filters Display
**Show active filters:**
- Above product grid
- Pill/tag format: "Blue ✕" "Under $50 ✕"
- Click X to remove individual filter
- "Clear All" link to remove all filters
- Count: "3 filters active"
## Sorting Strategy
### Common Sort Options
**Essential options:**
- **Featured** (default): Store's recommended order (bestsellers, promoted)
- **Price: Low to High**: Budget-conscious shoppers
- **Price: High to Low**: Premium product seekers
- **Newest**: Fashion, tech, time-sensitive products
- **Best Selling**: Social proof, popular choices
- **Top Rated**: Quality-focused shoppers
**Advanced options:**
- Name: A-Z (alphabetical)
- Discount: Highest % off (sale hunters)
- Reviews: Most reviewed (validation seekers)
### Sort Implementation
**Display:**
- Dropdown above product grid (right-aligned)
- Label: "Sort by:" or just dropdown
- Update products immediately on selection
- Show current sort in URL: `/products?order=-created_at`
**Backend integration:**
- Pass sort parameter to API (check backend docs for parameter name)
- Common parameters: `order`, `sort`, `sort_by`
- Common values: `-created_at` (desc), `+price` (asc), `-price` (desc)
**Preserve filters:**
- Sorting doesn't clear filters
- Maintains all active filters
- Updates URL with sort parameter
## Backend Integration
### Fetching Products
**Query parameters to include:**
- Category/collection filter (if applicable)
- Pagination (limit, offset or cursor)
- Sort order
- Filter values (price, attributes, etc.)
- For Medusa: `region_id` (required for correct pricing)
Check backend API documentation for exact parameter names and formats.
### Available Filters
**Dynamic filter updates:**
- Show only relevant filters for current category
- Display product count per filter option
- Gray out options with 0 products
- Update available options when filters change
### URL State Management
**Filter URL structure pattern:**
`/products?category_id=123&order=-created_at&page=2&price=0-50`
**Benefits:**
- Shareable links
- Bookmarkable searches
- Browser back/forward works correctly
- SEO-friendly (crawlable filter combinations)
**Implementation approach:**
- Read filters from URL query parameters on page load
- Update URL when filters change using URLSearchParams and history.pushState
- Parse URL parameters to reconstruct filter state
## Empty and No Results States
### No Products in Category
**When category is empty:**
- Message: "No products available yet"
- Subtext: "Check back soon for new arrivals"
- CTA: "Browse all products" or "Go to home"
- Alternative: Show related categories
- Optional: Newsletter signup for notifications
### No Results from Filters
**When filters too restrictive:**
- Message: "No products match your filters"
- Subtext: "Try removing some filters or adjusting your criteria"
- **Prominent "Clear All Filters" button**
- Show which filters might be too restrictive
- Suggestions: "Try expanding price range" or "Remove brand filter"
**Example:**
```
No products found
You filtered by:
- Color: Blue
- Size: XXL
- Price: $0-$20
Try:
• Removing size filter (only 2 XXL products)
• Expanding price range
• [Clear All Filters]
```
### No Results from Search
**When search query returns nothing:**
- Message: "No results for '[query]'"
- Suggestions: Check spelling, try different keywords
- CTA: Browse popular categories
- Show search suggestions (similar queries)
- Display popular or trending products
## Performance Optimization
### Lazy Loading Images
**Implementation:**
- Load images as they come into viewport
- Use Intersection Observer API
- Show placeholder or blur-up effect
- Improves initial page load significantly
**Critical for ecommerce:**
- Product listings have 24-100+ images per page
- Lazy loading reduces initial load by 60-80%
- Faster perceived performance
### Virtual Scrolling (Advanced)
**When to use:**
- Very large catalogs (500+ products visible)
- Infinite scroll with memory concerns
- Performance issues with many DOM elements
**How it works:**
- Only render visible products + buffer
- Reuse DOM elements as user scrolls
- Maintains scroll position
- Libraries: react-window, react-virtuoso
**Tradeoff:**
- Complex implementation
- Better performance for large lists
- Required for catalogs with 1000+ products loaded
### Filter Performance
**Optimistic UI:**
- Update grid immediately (predicted results)
- Show loading overlay briefly
- Replace with real results
- Better perceived performance
## Mobile Optimization
**Critical mobile patterns:**
**2-column grid:**
- Maximum 2 products per row
- Larger touch targets
- Simplified cards (essential info only)
- Remove hover effects
**Filter drawer:**
- Full-screen or 80% width drawer
- "Filters" button with badge count
- Batch apply (don't re-fetch on each change)
- Clear all at top
**Sticky filter/sort bar:**
- Fixed at top while scrolling
- Quick access to filters and sorting
- Shows active filter count
- Higher engagement rates
**Infinite scroll default:**
- Better mobile UX than pagination
- Natural scrolling behavior
- Keep pagination URLs for SEO
- Handle back button correctly
**Performance:**
- Lazy load images (critical on mobile)
- Limit initial products (12-24)
- Optimize image sizes for mobile
- Fast filter updates (<1s)
## Checklist
**Essential product listing features:**
- [ ] **RECOMMENDED: Product listing built as reusable component**
- [ ] Reusable component works for: shop all, category pages, search results, sale pages
- [ ] Component accepts filter parameters (categoryId, searchQuery, promotionId, etc.)
- [ ] Responsive grid (3-4 columns desktop, 2 mobile)
- [ ] Decision made: Pagination vs infinite scroll vs load more
- [ ] Filter pattern selected: Sidebar (desktop) vs drawer (mobile)
- [ ] Filters fetched from backend dynamically
- [ ] Filter options show product count
- [ ] Active filters displayed above grid (removable pills)
- [ ] "Clear all filters" button prominent
- [ ] Sorting options (featured, price, newest, bestselling)
- [ ] Sort updates products without clearing filters
- [ ] Filters and sort persist in URL (shareable)
- [ ] Results count displayed ("Showing 1-24 of 156 products")
- [ ] Empty state: "No products match filters" with suggestions
- [ ] "Clear all filters" prominent when no results
- [ ] Product prices displayed correctly (Medusa: as-is, not divided)
- [ ] Lazy loading for images (Intersection Observer)
- [ ] Loading state for filter changes (< 1s)
- [ ] Mobile: Filter drawer with batch apply
- [ ] Mobile: 2-column grid maximum
- [ ] Mobile: Sticky filter/sort button
- [ ] Pagination URLs for SEO (even with infinite scroll)
- [ ] Back button support (restore filters, scroll position)
- [ ] Keyboard accessible (tab through filters, enter to apply)
- [ ] ARIA labels on filters (role="group", aria-label)

View File

@@ -0,0 +1,356 @@
# Static Pages
## Contents
- [Overview](#overview)
- [FAQ Page](#faq-page)
- [About Page](#about-page)
- [Contact Page](#contact-page)
- [Shipping and Returns](#shipping-and-returns)
- [Privacy Policy and Terms](#privacy-policy-and-terms)
- [Size Guide](#size-guide)
- [Mobile and SEO](#mobile-and-seo)
- [Checklist](#checklist)
## Overview
Static pages provide essential information about the store, policies, and customer support. Purpose: Build trust, reduce support inquiries, meet legal requirements, improve SEO.
### Essential Static Pages
**Required:**
- Privacy Policy (legally required in most regions)
- Terms and Conditions
- Shipping and Returns
- Contact
**Strongly recommended:**
- FAQ (Frequently Asked Questions)
- About Us
**Optional:**
- Size Guide (for apparel stores)
- Store Locator (if physical stores)
## FAQ Page
### Purpose and Structure
**Purpose**: Answer common customer questions, reduce support inquiries, improve purchase confidence.
**Common FAQ categories:**
- Ordering and Payment
- Shipping and Delivery
- Returns and Exchanges
- Product Information
- Account Management
### Layout Pattern: Accordion (Recommended)
Question as clickable header, answer hidden by default, click to expand. Compact, scannable format.
**Example:**
```
Frequently Asked Questions
Ordering and Payment
▸ How do I place an order?
▸ What payment methods do you accept?
▸ Is it safe to use my credit card?
Shipping and Delivery
▸ How long does shipping take?
▸ Do you ship internationally?
```
**Alternative: All Expanded**
All questions and answers visible. Better for few questions (<10), good for SEO (all content visible), easy to Ctrl+F search.
### Search Functionality
**FAQ search** (for extensive FAQs):
Search box at top, real-time filtering as user types, highlights matching questions, "No results" state with contact link.
### Content Guidelines
**Question format:**
Clear, concise, use customer language, start with question words (How, What, When).
**Answer format:**
Direct answer first sentence, additional details if needed, bullet points for lists, link to related pages, 2-4 sentences ideal.
## About Page
### Purpose and Content
**Purpose**: Tell brand story, build trust and connection, showcase values and mission, differentiate from competitors.
**Key sections:**
- Brand story/history (how the company started)
- Mission and values (3-5 core values)
- Why choose us (what makes you different)
- Sustainability/social responsibility (if applicable)
### Layout Structure
**Hero section:**
Large image (team, products, brand imagery), headline (brand tagline or mission), brief intro paragraph (2-3 sentences).
**Story section:**
3-5 paragraphs, conversational tone, focus on customer benefits.
**Values section:**
3-5 core values with icon or image for each, brief description (1-2 sentences).
**Call-to-action:**
Shop products, join newsletter, follow on social media.
## Contact Page
### Contact Methods
**Essential:**
- Contact form (primary)
- Email address
- Phone number (optional)
- Business hours
- Response time expectation ("We respond within 24 hours")
**Optional:**
- Live chat button
- FAQ link ("Find answers faster")
- Social media links
- Physical address (if applicable)
### Contact Form
**Form fields:**
- Name (required)
- Email (required)
- Subject or Topic (dropdown, optional)
- Message (textarea, required)
- Order number (optional, for order inquiries)
- Submit button
**Form features:**
Clear field labels, placeholder examples, required field indicators, email validation, success confirmation.
## Shipping and Returns
### Shipping Information
**Key sections:**
- Shipping methods and costs (table format)
- Delivery timeframes
- International shipping (if applicable)
- Order processing time ("Orders placed by 2pm EST ship same day")
- Tracking information
- Shipping restrictions
**Shipping Methods Table Format:**
```
Method | Cost | Delivery Time
──────────────────────────────────────────
Standard | $5.99 | 5-7 business days
Express | $12.99 | 2-3 business days
Overnight | $24.99 | Next business day
Free Shipping | Free | Orders over $50
```
### Returns and Exchanges
**Key information:**
- Return window (e.g., 30 days)
- Return conditions (unused, tags attached, etc.)
- Refund method (original payment, store credit)
- Return shipping cost
- Exchange process
- Non-returnable items
**Return process steps:**
```
How to Return an Item
1. Initiate Return
Log into your account and select the order
2. Print Return Label
We'll email you a prepaid shipping label
3. Pack and Ship
Include all original packaging and tags
4. Receive Refund
Refunds processed within 5-7 business days
```
## Privacy Policy and Terms
### Privacy Policy
**Purpose**: Legal requirement in most regions (GDPR, CCPA compliance), explain data collection and use, build customer trust.
**Key sections:**
- Information collected
- How information is used
- Data sharing and disclosure
- Cookies and tracking
- User rights (access, deletion, etc.)
- Data security measures
- Contact for privacy inquiries
- Last updated date
**Layout:**
Table of contents (for long policies), clear section headings, numbered or bulleted lists, plain language, last updated date at top.
**Important**: Consult legal counsel for content, include required disclosures, update regularly.
### Terms and Conditions
**Purpose**: Legal agreement between store and customer, define rules and limitations, protect business legally.
**Key sections:**
- Acceptance of terms
- Product descriptions and pricing
- Order acceptance and cancellation
- Payment and billing
- Shipping and delivery
- Returns and refunds
- Intellectual property
- Limitation of liability
- Dispute resolution
- Changes to terms
**Layout:**
Numbered sections (1, 1.1, 1.2), table of contents for long documents, clear section titles, anchor links to sections.
## Size Guide
**Purpose (for apparel/footwear stores)**:
Help customers choose correct size, reduce returns due to sizing issues, increase purchase confidence.
**Content:**
- Size charts (numeric measurements) - use proper table markup
- How to measure instructions with illustrations
- Fit descriptions (slim fit, relaxed, etc.)
- Model measurements (for reference)
- Size conversion chart (US, EU, UK)
**Size Chart Format:**
```
Women's Tops Size Guide
Size | Bust | Waist | Hips
─────────────────────────────────────
XS | 32-33" | 24-25" | 35-36"
S | 34-35" | 26-27" | 37-38"
M | 36-37" | 28-29" | 39-40"
L | 38-40" | 30-32" | 41-43"
XL | 41-43" | 33-35" | 44-46"
```
**Accessibility:**
Use proper table markup (not images), clear column headers, screen reader friendly, mobile-responsive tables.
## Mobile and SEO
### Mobile Optimizations
**Layout:**
Single column, full-width content, larger touch targets for accordions, generous padding (16-20px).
**Typography:**
16px minimum body text, 24-32px headings, line height 1.5-1.6, short paragraphs (3-4 sentences max).
**Tables:**
Horizontal scroll for wide tables or card layout (stacked rows), responsive design.
**Forms:**
Full-width inputs, 44-48px height, large submit buttons, appropriate keyboard types.
**Quick actions:**
- Tap-to-call: Phone numbers as clickable links (`tel:`)
- Tap-to-email: Email addresses as clickable links (`mailto:`)
- Map integration: "Get Directions" links to native map app
### SEO for Static Pages
**On-Page SEO:**
- Unique title per page (50-60 characters max, format: "Page Title | Store Name")
- Meta descriptions (150-160 characters, include call-to-action)
- Proper heading hierarchy (one H1 per page, H2 for sections, H3 for subsections)
- Original, unique content
- Internal links to products/categories
- Regular updates (especially FAQ)
**Schema Markup:**
- FAQ schema for FAQ pages (rich snippets)
- Organization schema for About page
- LocalBusiness schema for Store Locator
- ContactPoint schema for Contact page
**Benefits:**
Rich snippets in search results, improved visibility, better click-through rates.
## Checklist
**Essential pages:**
- [ ] FAQ page with accordion layout
- [ ] FAQ search functionality (for extensive FAQs)
- [ ] FAQ categories organized logically
- [ ] Contact page with form
- [ ] Contact form validation
- [ ] Email address and business hours visible
- [ ] Shipping and Returns page
- [ ] Shipping methods and costs table
- [ ] Delivery timeframes clear
- [ ] Return policy with conditions
- [ ] Return process step-by-step
- [ ] Privacy Policy page
- [ ] Last updated date on Privacy Policy
- [ ] Privacy policy in plain language
- [ ] Terms and Conditions page
- [ ] Last updated date on Terms
**Optional but valuable:**
- [ ] About Us page with brand story
- [ ] Mission and values section
- [ ] Size Guide (if apparel)
- [ ] Size charts with measurements
- [ ] How to measure instructions
- [ ] Store Locator (if physical stores)
**SEO and technical:**
- [ ] Unique title tags per page
- [ ] Meta descriptions per page
- [ ] Proper heading hierarchy (H1, H2, H3)
- [ ] Internal links to products/categories
- [ ] Schema markup (FAQ, Organization, etc.)
- [ ] Mobile-responsive layout
- [ ] Fast loading times
**Accessibility:**
- [ ] Semantic HTML (main, article, section)
- [ ] ARIA labels on accordions
- [ ] Keyboard navigation supported
- [ ] Focus indicators visible
- [ ] High contrast text (4.5:1)
- [ ] Forms properly labeled
- [ ] Tables with proper headers
- [ ] Alt text on images
**Content quality:**
- [ ] Clear, concise writing
- [ ] Short paragraphs (3-4 sentences)
- [ ] Bullet points for lists
- [ ] Regular content updates
- [ ] No outdated information
- [ ] Contact info current
- [ ] Policies reflect current practices

View File

@@ -0,0 +1,307 @@
# Medusa Backend Integration
## Contents
- [Overview](#overview)
- [Installation](#installation)
- [SDK Setup](#sdk-setup)
- [Vite Configuration](#vite-configuration-tanstack-start-vite-projects)
- [TypeScript Types](#typescript-types)
- [Price Display](#price-display)
- [SDK Organization](#sdk-organization)
- [Critical Medusa Patterns](#critical-medusa-patterns)
- [Region State Management](#region-state-management)
## Overview
Guide for connecting your storefront to Medusa backend using the [Medusa JS SDK](https://docs.medusajs.com/resources/js-sdk).
**When to use this guide:**
- Building a storefront with Medusa backend
- Need to integrate Medusa SDK properly
- Working with multi-region stores
- Handling Medusa-specific pricing and regions
**For general backend patterns**, see `reference/connecting-to-backend.md`.
## ⚠️ CRITICAL: Follow the 5-Step Verification Workflow
**BEFORE writing code that calls Medusa SDK methods**, follow the mandatory workflow from SKILL.md:
1. **PAUSE** - Don't write code yet
2. **QUERY** MCP server or docs (https://docs.medusajs.com/resources/js-sdk) for exact method
3. **VERIFY** with user what you found
4. **WRITE** code using verified method
5. **CHECK** for TypeScript errors - Type errors mean wrong method name or parameters
**If you see TypeScript errors on SDK methods, you used incorrect methods. Go back to Step 2 and verify again.**
**This file shows PATTERNS (what to do), not exact methods (how to do it). Always verify method names with MCP/docs before use.**
## 💡 RECOMMENDED: Set Up Medusa MCP Server
**If the Medusa MCP server is not installed, strongly recommend setting it up.**
**Setup instructions**: add HTTP MCP server with URL https://docs.medusajs.com/mcp
The MCP server provides real-time method verification without leaving your IDE.
## Installation
```bash
npm install @medusajs/js-sdk@latest @medusajs/types@latest
```
Both required: SDK provides functionality, types provide TypeScript support.
## SDK Setup
```typescript
import Medusa from "@medusajs/js-sdk"
export const sdk = new Medusa({
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL || "http://localhost:9000",
debug: process.env.NODE_ENV === "development",
publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})
```
**CRITICAL: Always set publishableKey.**
- Required for multi-region stores to get correct pricing
- Required for accessing products with regional prices
- Without it, product queries may fail or return incorrect prices
- Get publishable key from Medusa admin dashboard under Settings → Publishable API Keys
**IMPORTANT: Storefront Port Configuration**
- **Run storefront at port 8000** to avoid CORS errors
- Medusa backend's default CORS configuration expects storefront at `http://localhost:8000`
- If using different port, configure CORS in Medusa backend's `medusa-config.ts`:
```typescript
store_cors: process.env.STORE_CORS || "http://localhost:YOUR_PORT"
```
- Common framework defaults:
- Next.js: Port 3000 (needs CORS config update)
- TanStack Start: Port 3000 (needs CORS config update)
- Vite: Port 5173 (needs CORS config update)
- **Recommended**: Use port 8000 to avoid configuration changes
## Vite Configuration (TanStack Start, Vite Projects)
**IMPORTANT: For Vite-based projects, configure SSR externals.**
Add this to your `vite.config.ts`:
```typescript
export default defineConfig({
// ... other config
ssr: {
noExternal: ['@medusajs/js-sdk'],
},
})
```
**Why this is needed:**
- Medusa JS SDK must be processed by Vite during SSR
- Without this config, SDK calls will fail during server-side rendering
- Applies to TanStack Start, vanilla Vite, and other Vite-based frameworks
## TypeScript Types
**IMPORTANT: Always use `@medusajs/types` - never define custom types.**
```typescript
import type {
StoreProduct,
StoreCart,
StoreCartLineItem,
StoreRegion,
StoreProductCategory,
StoreCustomer,
StoreOrder
} from "@medusajs/types"
```
**Why use official types:**
- Complete and accurate type definitions
- Updated with each Medusa release
- Includes all entity relationships and fields
- Prevents type mismatches with API responses
## Price Display
**CRITICAL: Medusa prices are stored as-is - DO NOT divide by 100.**
Unlike Stripe (where amounts are in cents), Medusa stores prices in their display value.
```typescript
// ❌ WRONG - Dividing by 100
<div>${product.variants[0].prices[0].amount / 100}</div>
// ✅ CORRECT - Display as-is
<div>${product.variants[0].prices[0].amount}</div>
```
**Correct price formatting:**
```typescript
const formatPrice = (amount: number, currencyCode: string) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currencyCode,
}).format(amount)
}
```
**Price fields to use:**
- `variant.calculated_price.calculated_amount` - Final price including promotions
- `variant.calculated_price.original_amount` - Original price before discounts
- Both are already in display format - no conversion needed
## SDK Organization
The Medusa SDK is organized by resources:
- `sdk.store.product.*` - Product operations
- `sdk.store.cart.*` - Cart operations
- `sdk.store.category.*` - Category operations
- `sdk.store.customer.*` - Customer operations (authenticated)
- `sdk.store.order.*` - Order operations (authenticated)
- `sdk.store.payment.*` - Payment operations
- `sdk.store.fulfillment.*` - Shipping/fulfillment operations
- `sdk.store.region.*` - Region operations
**To find specific methods**: Consult documentation (https://docs.medusajs.com/resources/js-sdk) or use MCP server.
## Critical Medusa Patterns
**IMPORTANT**: The patterns below show WHAT to do, not exact HOW. Always verify method names and signatures with MCP server or documentation before using.
### 1. Always Pass `region_id` for Products
**Pattern**: Product queries require `region_id` parameter for correct pricing.
**Why:** Without `region_id`, `calculated_price` will be missing or incorrect.
**To implement**: Query MCP/docs for product listing and retrieval methods. Pass `region_id: selectedRegion.id` as parameter.
### 2. Cart Updates Pattern
**Pattern**: Line items have dedicated methods (create, update, delete). Other cart properties use a generic update method.
**Line item operations** (verify exact method names with MCP/docs):
- Add item to cart
- Update item quantity
- Remove item from cart
**Other cart updates** (email, addresses, region, promo codes):
- Use cart's generic update method
**To implement**: Query MCP server or documentation for exact cart method signatures:
https://docs.medusajs.com/resources/references/js-sdk/store/cart
### 3. Payment Flow Pattern
**High-level workflow:**
1. Query available payment providers for the cart's region
2. User selects payment method
3. Initialize payment session for selected provider
4. Render provider-specific UI (Stripe Elements, etc.)
5. Complete payment through provider
**To implement**: Query MCP/docs for:
- Payment provider listing method
- Payment session initialization method
- Payment completion method
**Resources**:
- MCP server (if installed)
- Medusa payment docs: https://docs.medusajs.com/resources/references/js-sdk/store/payment
- `reference/layouts/checkout.md` for checkout flow
### 4. Checkout Flow Pattern
**High-level workflow:**
1. Collect shipping address
2. Query available shipping options for cart
3. User selects shipping method
4. Collect payment information
5. Initialize payment session
6. Complete/place order
**To implement**: Query MCP/docs for each step's methods. Don't guess method names.
### 5. Category Fetching
**Pattern**: Fetch categories from `sdk.store.category.*` resource.
**To implement**: Query MCP/docs for category listing method. See `reference/components/navbar.md` for usage patterns.
## Region State Management
**Critical for Medusa**: Region determines currency, pricing, taxes, and available products.
### Why Region Context Matters
Medusa requires region for:
- Creating carts (must pass `region_id`)
- Retrieving products with correct prices
- Determining currency and tax calculations
- Filtering available payment and shipping methods
### Implementation Approach
**High-level workflow:**
1. Fetch available regions on app load (query MCP/docs for region listing method)
2. Detect user's country (IP, browser locale, or user selection)
3. Find region containing that country
4. Store selected region globally (React Context, Zustand, etc.)
5. Use `selectedRegion.id` for all cart and product operations
**When user changes country:**
- Find new region containing the country
- Update cart with new region_id (query MCP/docs for cart update method)
- Store selection in localStorage for persistence
**To implement**: Query MCP server or docs for exact region and cart methods. Don't copy example code without verification.
**For detailed region implementation with code examples**, see:
- `reference/components/country-selector.md`
- Medusa MCP server (if installed)
- Medusa docs: https://docs.medusajs.com/resources/storefront-development/regions/context
## Error Handling
SDK throws `FetchError` with:
- `status`: HTTP status code
- `statusText`: Error code
- `message`: Descriptive message
```typescript
try {
const data = await sdk.store.customer.retrieve()
} catch (error) {
const fetchError = error as FetchError
if (fetchError.statusText === "Unauthorized") {
redirect('/login')
}
}
```
## Custom Endpoints
For custom API routes:
```typescript
const data = await sdk.client.fetch(`/custom/endpoint`, {
method: "POST",
body: { /* ... */ },
})
```
## Resources
- **Medusa JS SDK docs**: https://docs.medusajs.com/resources/js-sdk
- **Storefront development**: https://docs.medusajs.com/resources/storefront-development
- **Checkout flow**: https://docs.medusajs.com/resources/storefront-development/checkout
- **Region context**: https://docs.medusajs.com/resources/storefront-development/regions/context
- **Use Medusa MCP server** if available for real-time method lookup

View File

@@ -0,0 +1,183 @@
# Mobile Responsiveness for Ecommerce Storefronts
## Contents
- [Overview](#overview)
- [Mobile Ecommerce Patterns](#mobile-ecommerce-patterns)
- [Touch-Friendly Interactions](#touch-friendly-interactions)
- [Mobile Performance](#mobile-performance)
- [Safe Area Insets (iOS)](#safe-area-insets-ios)
- [Common Mobile Mistakes](#common-mobile-mistakes)
## Overview
Over 60% of ecommerce traffic is mobile. Mobile-first design is essential for conversion.
### Key Requirements
- Mobile-first CSS (min-width media queries)
- 44x44px minimum touch targets
- Sticky header with cart access
- Large form inputs (48px height minimum)
- Optimized images for mobile
- Fast loading (LCP < 2.5s)
**Assumed knowledge**: AI agents already know mobile-first design principles, breakpoints, and responsive CSS. This guide focuses on ecommerce-specific mobile patterns.
## Mobile Ecommerce Patterns
### Sticky Elements (Critical for Conversion)
**Cart access always visible:**
- Sticky header with cart icon (top-right)
- Or: Sticky bottom navigation with cart
- Never hide cart in hamburger drawer
- Shows count badge, updates in real-time
**Sticky "Add to Cart" bar (product pages):**
- Fixed at bottom of screen
- Shows: Price + "Add to Cart" button
- Appears after scrolling past fold
- Always accessible without scrolling
- **CRITICAL: Must use `env(safe-area-inset-bottom)` for iOS devices** (see Safe Area Insets section)
- Significantly higher conversion rates
### Mobile Navigation Patterns
**Bottom navigation (optional pattern):**
- Consider for mobile-heavy stores (>70% mobile traffic)
- 4-5 primary actions: Home, Categories, Cart, Account, Search
- Fixed at bottom (easier thumb access)
- Icons + labels for clarity
**When to use:**
- Mobile-first brands (fashion, beauty)
- Younger demographic (18-34)
- App-like experience desired
**When NOT to use:**
- Desktop-heavy traffic
- Complex navigation needs (>5 items)
- B2B stores (desktop-focused)
### Mobile Product Browsing
**Image galleries:**
- Full-width swipeable carousel
- Pinch to zoom
- Tap to open full-screen view
- Dot indicators (1/5, 2/5)
**Filter drawer:**
- "Filters" button with badge count (e.g., "Filters (3)")
- Slide-out drawer (full-screen or 80% width)
- Accordion sections for filter categories
- "Apply" button at bottom (batch filtering)
- "Clear All" option at top
**Why batch filtering on mobile:**
- Prevents multiple re-renders on slow connections
- User adjusts multiple filters before applying
- Less disruptive mobile UX
### Mobile Checkout Optimization
**Digital wallets priority (CRITICAL for mobile conversion):**
- Apple Pay / Google Pay buttons prominent at top (if supported in ecommerce backend)
- Can improve mobile checkout conversion by 20-40%
- One-click payment with pre-filled shipping addresses (if supported in ecommerce backend)
- Consider making digital wallet the default on mobile
**Decision: Order summary placement**
- Collapsible at top (recommended): Saves screen space for form, expandable for review
- Fixed at bottom: Always visible but takes space from form
- Use collapsible on mobile to prioritize form completion
**Form optimizations:**
- Single-column layout (never multi-column on mobile)
- 44-48px input height minimum
- Proper keyboard types (`inputMode="email"`, `"numeric"`, `"tel"`)
- Autocomplete attributes for autofill (`autocomplete="email"`, `"name"`, `"address-line1"`)
- Consider single-page layout over multi-step (less friction on mobile)
## Touch-Friendly Interactions
**Standard touch targets:** 44x44px minimum for all interactive elements. Pay special attention to:
- Filter checkboxes on product listings
- Quantity +/- buttons on product pages
- Small action buttons on product cards
- Modal close buttons
**Swipe gestures for ecommerce:**
- Product image galleries (critical - users expect swipeable images)
- Related product sliders
- Category carousels
**Mobile input optimization:**
- 16px minimum font size for inputs (prevents iOS auto-zoom)
- Proper `inputMode` attributes: `"email"`, `"numeric"`, `"tel"`
- Autocomplete attributes: `autocomplete="email"`, `"name"`, `"address-line1"`
## Mobile Performance
**Ecommerce performance priorities:**
1. **Product images** (highest impact): Optimize for mobile (<500KB), lazy load below-fold, responsive images with appropriate sizes
2. **Optimistic UI**: Cart count updates immediately, instant feedback on add to cart
3. **Skeleton screens**: Show loading placeholders for product grids, not blank pages
**Critical mobile performance issues:**
- Unoptimized product images (>1MB) - most common issue
- Loading entire product catalog at once - use pagination or infinite scroll
- Heavy analytics scripts on checkout - defer to post-purchase
**Target**: LCP < 2.5s, mobile-optimized images, server-side rendering for product pages
## Safe Area Insets (iOS)
Use `env(safe-area-inset-*)` to handle iOS notches and rounded corners on:
- Sticky headers (top inset)
- Sticky bottom navigation or add-to-cart bars (bottom inset)
- Full-screen modals
**Critical for ecommerce**: Bottom "Add to Cart" bars will be cut off by iOS home indicator without bottom inset (~34px). Test on real iOS devices with notches.
## Common Mobile Mistakes
**Ecommerce-specific mobile issues:**
1. **Hiding cart in drawer** - Cart icon hidden in hamburger menu. Keep cart always visible in header (top-right).
2. **No sticky cart access** - Cart scrolls off screen on product pages. Use sticky header or sticky bottom "Add to Cart" bar.
3. **Desktop-sized images** - Serving 2MB+ product images to mobile. Use responsive images optimized for mobile (<500KB).
4. **Poor form experience** - Small inputs, wrong keyboards, no autocomplete. Use 48px inputs, proper `inputMode`, autocomplete attributes.
5. **Hover-only interactions** - Quick view, wishlist only work on hover. Add tap handlers, show on tap instead.
6. **Ignoring safe area insets** - Bottom "Add to Cart" bars cut off by iOS home indicator. Use `env(safe-area-inset-bottom)` for sticky bottom elements.
7. **No digital wallet options** - Missing Apple Pay/Google Pay on mobile checkout. Mobile users expect one-tap checkout options.
## Mobile Checklist
**Essential mobile optimizations:**
- [ ] Mobile-first CSS (min-width media queries)
- [ ] 44x44px minimum touch targets throughout
- [ ] Adequate spacing between interactive elements (8-16px)
- [ ] Sticky header with cart icon (always visible)
- [ ] Or: Sticky bottom "Add to Cart" bar on product pages
- [ ] Large form inputs (48px height minimum)
- [ ] Appropriate input types (`inputMode="email"`, `"numeric"`, `"tel"`)
- [ ] Swipeable image galleries on product pages
- [ ] Filter drawer with batch apply on product listings
- [ ] Digital wallets prominent in checkout (Apple Pay, Google Pay)
- [ ] Collapsible order summary in checkout
- [ ] Optimized images for mobile (<500KB)
- [ ] Lazy loading for below-fold content
- [ ] Safe area insets for iOS notches (sticky elements)
- [ ] 16px minimum font size (prevents iOS auto-zoom)
- [ ] Test on real mobile devices (not just Chrome DevTools)
- [ ] Core Web Vitals within targets (LCP < 2.5s, CLS < 0.1, INP < 200ms)

View File

@@ -0,0 +1,195 @@
# SEO Optimization for Ecommerce Storefronts
## Contents
- [Overview](#overview)
- [Meta Tags Requirements](#meta-tags-requirements)
- [Structured Data (Critical for Ecommerce)](#structured-data-critical-for-ecommerce)
- [Ecommerce URL Patterns](#ecommerce-url-patterns)
- [Product Page SEO](#product-page-seo)
- [Duplicate Content Issues](#duplicate-content-issues)
- [Dynamic Sitemaps](#dynamic-sitemaps)
- [Common SEO Mistakes](#common-seo-mistakes)
## Overview
SEO is critical for ecommerce - organic search drives high-intent traffic. Proper implementation helps search engines understand products and enables rich results (star ratings, price, availability in search).
**Assumed knowledge**: AI agents know basic SEO (meta tags, Open Graph, Core Web Vitals). This guide focuses on ecommerce-specific patterns.
### Every Product Page Needs
- Unique title and description (product name + features)
- Product schema with price, availability, rating
- Breadcrumb schema (category hierarchy)
- Canonical URL (prevents duplicate content)
- Descriptive image alt text
- Fast load time (LCP < 2.5s)
## Meta Tags Requirements
Generate unique meta tags for every product page dynamically from product data:
- **Title**: "Product Name - Key Feature | Store Name" (50-60 characters)
- **Description**: Key features + USP (150-160 characters)
- **Open Graph** tags for social sharing (image 1200x630px)
- **Canonical URL** for variants and category paths
**Common mistake**: Same title/description across all products. Generate dynamically from product data.
## Structured Data (Critical for Ecommerce)
Enables rich results in search (star ratings, price, availability shown directly in search results).
### Product Schema (Required on All Product Pages)
Implement schema.org Product structured data with these critical fields:
**Essential fields:**
- `name`, `description`, `image`, `sku`, `brand`
- `offers` object with:
- `price`: Current price (Medusa: use as-is; other backends: check format)
- `priceCurrency`: ISO 4217 code (USD, EUR, GBP)
- `availability`: Must accurately reflect real stock status (InStock, OutOfStock, PreOrder)
- `priceValidUntil`: Required for Google Shopping (set 30+ days in future)
**Critical**: `availability` must be dynamic and accurate. Marking out-of-stock items as InStock violates Google's guidelines.
### AggregateRating Schema (When Reviews Exist)
Add `aggregateRating` object to Product schema when real reviews exist:
- `ratingValue`: Average rating (e.g., "4.5")
- `reviewCount`: Total number of reviews
- `bestRating`: "5", `worstRating`: "1"
Displays star ratings in search results - powerful for CTR. **Only use for real reviews** - fake reviews violate guidelines.
### Breadcrumb Schema (Navigation Hierarchy)
Implement schema.org BreadcrumbList showing category hierarchy:
- Home → Category → Product
- Each level has `position`, `name`, `item` URL
- Helps search engines understand site structure
### Organization Schema (Homepage Only)
Add on homepage only: Organization name, URL, logo, contact information. Helps establish brand identity in search.
## Ecommerce URL Patterns
**Product URLs**: Use readable slugs with hyphens (`/products/wireless-headphones-pro`). Include keywords naturally, keep short (<75 characters), never change URLs.
**Category URLs**: Choose consistent structure:
- Hierarchical (`/categories/electronics/laptops`) for deep catalogs
- Flat (`/categories/laptops`) for simpler management
- Don't mix both approaches
**Pagination URLs**: Use query parameters (`/products?page=2`). Implement `rel="prev"` and `rel="next"` tags. Each page is canonical to itself (NOT to page 1) so all pages can be indexed.
**Filter URLs**: Use query parameters (`/products?color=blue&size=large`).
**Canonical decision for filters:**
- Few filters (2-3): Index filtered pages (primary navigation)
- Many filters (5+): Canonical to unfiltered (prevents duplicate content)
- Popular combinations: Consider indexing separately
## Product Page SEO
**Title tags**: Pattern "Product Name - Key Feature | Store Name" (50-60 characters). Avoid generic titles or keyword stuffing.
**Meta descriptions**: Include 2-3 key features + USP (free shipping, returns, warranty) in 150-160 characters. Make it compelling.
**Image alt text**: Descriptive and specific. Describe what's visible, include product name and key visual attributes. Don't keyword stuff. Keep under 125 characters.
## Duplicate Content Issues
### Ecommerce Duplicate Content Challenges
**Common scenarios:**
1. **Product variants**: Same product in multiple colors/sizes
2. **Multiple categories**: Product listed in multiple categories
3. **Filter combinations**: Filtered views create unique URLs
4. **Sort parameters**: Sorted views create unique URLs
### Solution: Canonical URLs
**Variant handling:**
- Choose one variant as canonical (usually default)
- All other variants canonical to that one
- Example: Blue, Red, Green shirts all canonical to Blue (default)
**Multiple category paths:**
- Choose one category as canonical (usually primary category)
- Example: Product in both "Electronics" and "Laptops" → canonical to "Laptops"
**Filtered/sorted views:**
- Canonical to unfiltered, default-sorted page
- Example: `/products?color=blue&sort=price` → canonical to `/products`
**Implementation:**
```html
<!-- On variant page (Red shirt) -->
<link rel="canonical" href="https://yourstore.com/products/cotton-tshirt" />
<!-- On filtered page -->
<link rel="canonical" href="https://yourstore.com/products" />
```
## Dynamic Sitemaps
Generate sitemaps dynamically from database to help search engines discover all products and categories.
**Requirements:**
- Include all public product and category pages
- Update `lastModified` when products change (fetch from database)
- Exclude `noindex` pages and filtered/sorted URLs
- Split into multiple sitemaps if >50,000 URLs
- Priority: Homepage (1.0) > Products (0.8) > Categories (0.6)
**Critical**: Regenerate sitemap when products are added/updated. Submit sitemap URL to Google Search Console.
## Common SEO Mistakes
**Ecommerce-specific SEO issues:**
1. **Duplicate content** - Same product accessible via multiple URLs (variants, categories). Use canonical URLs to consolidate signals.
2. **Missing Product schema** - No structured data on product pages. Implement Product schema for rich results (ratings, price in search).
3. **Incorrect availability status** - Marking out-of-stock items as "InStock" in schema. Dynamically set based on real stock levels (violates Google guidelines).
4. **Thin product content** - Only image and price, no description. Add detailed descriptions, specifications, reviews (200+ words).
5. **Static sitemap** - Never updates when products change. Generate dynamically from database so search engines discover new products.
6. **Poor image alt text** - Missing or generic "product image". Use descriptive alt text for image search traffic.
## SEO Checklist
### On Every Product Page
- [ ] Unique, descriptive title tag (50-60 characters)
- [ ] Unique, compelling meta description (150-160 characters)
- [ ] Open Graph and Twitter Card tags
- [ ] Product schema with price, availability, rating (if reviews exist)
- [ ] Breadcrumb schema (category hierarchy)
- [ ] Descriptive alt text on all images
- [ ] Canonical URL (especially for variants)
- [ ] Fast load time (LCP < 2.5s)
- [ ] Mobile-responsive design
- [ ] Internal links to related products/categories
- [ ] Detailed product description (200+ words ideal)
### Site-wide
- [ ] Dynamic XML sitemap submitted to search engines
- [ ] Robots.txt properly configured
- [ ] SSL certificate (HTTPS)
- [ ] Mobile-friendly design (44px touch targets)
- [ ] Organization schema on homepage
- [ ] Proper heading hierarchy (H1 for product title)
- [ ] Pagination implemented with rel="prev/next"
- [ ] Canonical URLs for filtered/sorted pages
- [ ] 404 pages with helpful navigation
- [ ] Image optimization (WebP, lazy loading)
- [ ] Core Web Vitals within targets (LCP < 2.5s, CLS < 0.1, INP < 200ms)

1
dashboard Submodule

Submodule dashboard added at a83ede5e95

34
docker-compose.yml Normal file
View File

@@ -0,0 +1,34 @@
services:
memgraph:
image: memgraph/memgraph-platform:latest
container_name: suplementos-memgraph
ports:
- "7687:7687" # Bolt (neo4j-driver connects here)
- "7444:7444" # Memgraph Lab UI
- "3000:3000" # Memgraph Lab web UI
volumes:
- memgraph_data:/var/lib/memgraph
- memgraph_log:/var/log/memgraph
- memgraph_etc:/etc/memgraph
environment:
- MEMGRAPH_TELEMETRY_ENABLED=false
restart: unless-stopped
qdrant:
image: qdrant/qdrant:latest
container_name: suplementos-qdrant
ports:
- "6333:6333" # HTTP REST API
- "6334:6334" # gRPC
volumes:
- qdrant_data:/qdrant/storage
environment:
- QDRANT__SERVICE__HTTP_PORT=6333
- QDRANT__SERVICE__GRPC_PORT=6334
restart: unless-stopped
volumes:
memgraph_data:
memgraph_log:
memgraph_etc:
qdrant_data:

83
infra/setup.sh Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -e
BOLD="\033[1m"
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
RED="\033[0;31m"
RESET="\033[0m"
echo -e "${BOLD}SupplementEvidence Engine — Setup${RESET}"
echo "======================================"
# ── 1. Check Docker ─────────────────────────────────────────────────────────
echo -e "\n${BOLD}[1/4] Verificando Docker...${RESET}"
if ! command -v docker &>/dev/null; then
echo -e "${RED}✗ Docker no está instalado. Instálalo desde https://www.docker.com${RESET}"
exit 1
fi
echo -e "${GREEN}✓ Docker disponible${RESET}"
# ── 2. Start Memgraph + Qdrant ───────────────────────────────────────────────
echo -e "\n${BOLD}[2/4] Levantando Memgraph y Qdrant...${RESET}"
cd "$(dirname "$0")/.."
docker compose up -d
echo -e "${GREEN}✓ Memgraph corriendo en bolt://localhost:7687${RESET}"
echo -e "${GREEN}✓ Memgraph Lab UI en http://localhost:3000${RESET}"
echo -e "${GREEN}✓ Qdrant corriendo en http://localhost:6333${RESET}"
# Wait for Memgraph to be ready
echo -e "\n Esperando a Memgraph..."
for i in {1..20}; do
if docker exec suplementos-memgraph mg_client --username "" --password "" --execute "RETURN 1" &>/dev/null 2>&1; then
echo -e "${GREEN}✓ Memgraph listo${RESET}"
break
fi
sleep 2
if [ $i -eq 20 ]; then
echo -e "${YELLOW}⚠ Memgraph tardando en iniciar — continúa, se conectará cuando esté listo${RESET}"
fi
done
# ── 3. Check Ollama ──────────────────────────────────────────────────────────
echo -e "\n${BOLD}[3/4] Verificando Ollama...${RESET}"
if ! command -v ollama &>/dev/null; then
echo -e "${RED}✗ Ollama no está instalado.${RESET}"
echo -e " Instálalo desde: ${BOLD}https://ollama.ai${RESET}"
exit 1
fi
if ! curl -s http://192.168.68.67:11434/api/tags &>/dev/null; then
echo -e "${YELLOW}⚠ Ollama no está corriendo. Iniciando...${RESET}"
ollama serve &>/dev/null &
sleep 3
fi
echo -e "${GREEN}✓ Ollama disponible en http://192.168.68.67:11434${RESET}"
# ── 4. Pull Ollama models ────────────────────────────────────────────────────
echo -e "\n${BOLD}[4/4] Descargando modelos de Ollama...${RESET}"
pull_model() {
local model=$1
echo -e "\n Verificando ${BOLD}${model}${RESET}..."
if ollama list 2>/dev/null | grep -q "^${model}"; then
echo -e "${GREEN}${model} ya está disponible${RESET}"
else
echo -e "${YELLOW} ↓ Descargando ${model} (puede tardar varios minutos)...${RESET}"
ollama pull "$model"
echo -e "${GREEN}${model} descargado${RESET}"
fi
}
pull_model "mxbai-embed-large"
pull_model "qwen3:8b"
# ── Done ─────────────────────────────────────────────────────────────────────
echo -e "\n${GREEN}${BOLD}✓ Infraestructura lista${RESET}"
echo ""
echo -e " Memgraph Lab: ${BOLD}http://localhost:3000${RESET}"
echo -e " Qdrant UI: ${BOLD}http://localhost:6333/dashboard${RESET}"
echo -e " Ollama: ${BOLD}http://localhost:11434${RESET}"
echo ""
echo -e " Siguiente: ${BOLD}cd dashboard && npm run dev${RESET}"

40
skills-lock.json Normal file
View File

@@ -0,0 +1,40 @@
{
"version": 1,
"skills": {
"building-admin-dashboard-customizations": {
"source": "medusajs/medusa-agent-skills",
"sourceType": "github",
"computedHash": "4cd60098a12fcfd682ae4f7668bd1cf0c5986c27e2ebfcf45166e3d69c4e5f1c"
},
"building-storefronts": {
"source": "medusajs/medusa-agent-skills",
"sourceType": "github",
"computedHash": "288f645ffd2b29f925f5c70077b9e57af6b787843fc98b276e84150d253511a7"
},
"building-with-medusa": {
"source": "medusajs/medusa-agent-skills",
"sourceType": "github",
"computedHash": "fef62d911eba65523b679baea2ce9bf1c253211f18499115374dc9727c024e4f"
},
"db-generate": {
"source": "medusajs/medusa-agent-skills",
"sourceType": "github",
"computedHash": "054793865ece11794c7d0738304257775ca9a86460301e91233f4633c06cc9c5"
},
"db-migrate": {
"source": "medusajs/medusa-agent-skills",
"sourceType": "github",
"computedHash": "67e08aa2633481106be84d196381320a4ba88410e6d710395234e23e394cd07e"
},
"new-user": {
"source": "medusajs/medusa-agent-skills",
"sourceType": "github",
"computedHash": "8474aef2e05fc8cf48e793d3ca1eca74095bfe23e8be8242b7c4613d401f52ac"
},
"storefront-best-practices": {
"source": "medusajs/medusa-agent-skills",
"sourceType": "github",
"computedHash": "633f757031071344075cce6450ff8c5bf209691a8d072bb6cc73be153faa4ba4"
}
}
}