diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index f9b7cd4b..86887a8f 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -28,6 +28,19 @@ }, "category": "Productivity" }, + { + "name": "storefront-next", + "description": "Storefront Next development skills for building React 19 storefronts on Salesforce B2C Commerce.", + "source": { + "source": "local", + "path": "./skills/storefront-next" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + }, { "name": "cap-dev", "description": "Skills for scaffolding, packaging, validating, and submitting Commerce App Packages (CAPs) for Salesforce Commerce Cloud.", diff --git a/.changeset/storefront-next-cli-sdk-support.md b/.changeset/storefront-next-cli-sdk-support.md new file mode 100644 index 00000000..c4fb90c4 --- /dev/null +++ b/.changeset/storefront-next-cli-sdk-support.md @@ -0,0 +1,6 @@ +--- +'@salesforce/b2c-tooling-sdk': minor +'@salesforce/b2c-cli': minor +--- + +Add `storefront-next` skill set to `b2c setup skills`. Install Storefront Next development skills with `b2c setup skills storefront-next`, or via the plugin marketplace with `claude plugin install storefront-next`. diff --git a/.changeset/storefront-next-skills-plugin.md b/.changeset/storefront-next-skills-plugin.md new file mode 100644 index 00000000..e88f91a6 --- /dev/null +++ b/.changeset/storefront-next-skills-plugin.md @@ -0,0 +1,5 @@ +--- +'@salesforce/b2c-agent-plugins': minor +--- + +Add `storefront-next` plugin with 14 skills covering the full Storefront Next development lifecycle — project setup, routing, data fetching, components, Page Designer, authentication, i18n, extensions, testing, performance, and deployment to Managed Runtime. diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 150cefdf..6a1a9997 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -42,6 +42,18 @@ "category": "productivity", "strict": false }, + { + "name": "storefront-next", + "description": "Storefront Next development skills for building React 19 storefronts on Salesforce B2C Commerce.", + "author": { + "name": "Salesforce" + }, + "license": "Apache-2.0", + "source": "./skills/storefront-next", + "category": "productivity", + "strict": false, + "version": "1.1.2" + }, { "name": "cap-dev", "description": "Skills for scaffolding, packaging, validating, and submitting Commerce App Packages (CAPs) for Salesforce Commerce Cloud.", diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5a4adcd6..98c18643 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -434,6 +434,9 @@ jobs: # Create b2c-cli-skills.zip containing skills/b2c-cli/skills/ cd skills/b2c-cli && zip -r ../../b2c-cli-skills.zip skills/ cd ../.. + # Create storefront-next-skills.zip containing skills/storefront-next/skills/ + cd skills/storefront-next && zip -r ../../storefront-next-skills.zip skills/ + cd ../.. echo "Created skills artifacts:" ls -la *.zip @@ -446,7 +449,7 @@ jobs: --title "Agent Plugins ${{ steps.packages.outputs.version_plugins }}" \ --notes "Skills artifacts for b2c-agent-plugins v${{ steps.packages.outputs.version_plugins }}" - gh release upload "$RELEASE_TAG" b2c-skills.zip b2c-cli-skills.zip + gh release upload "$RELEASE_TAG" b2c-skills.zip b2c-cli-skills.zip storefront-next-skills.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 871de89f..9ec50893 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -186,7 +186,7 @@ Changeset guidelines: - HOW a consumer should update their code - Good changesets are brief and user-focused (not contributor); they are generally 1 line or two; The content of the changeset is used in CHANGELOG and release notes. You do not need to list internal implementation details or all details of commands; just the high level summary for users. -Valid changeset packages: `@salesforce/b2c-cli`, `@salesforce/b2c-tooling-sdk`, `@salesforce/b2c-dx-mcp`, `@salesforce/mrt-utilities`, `b2c-vs-extension`, `@salesforce/b2c-dx-docs` +Valid changeset packages: `@salesforce/b2c-cli`, `@salesforce/b2c-tooling-sdk`, `@salesforce/b2c-dx-mcp`, `@salesforce/mrt-utilities`, `b2c-vs-extension`, `@salesforce/b2c-dx-docs`, `@salesforce/b2c-agent-plugins` Create a changeset file directly in `.changeset/` with a unique filename (e.g., `descriptive-change-name.md`): @@ -201,3 +201,4 @@ Description of the change explaining WHAT, WHY, and HOW to update - Include only the packages that were directly modified - For doc-only changes, target `@salesforce/b2c-dx-docs` instead of the CLI/SDK/MCP packages +- For changes to agent skills/plugins in `skills/` (adding or updating skill content, adding a new plugin), target `@salesforce/b2c-agent-plugins` diff --git a/docs/cli/setup.md b/docs/cli/setup.md index f8dbc93c..34a827a2 100644 --- a/docs/cli/setup.md +++ b/docs/cli/setup.md @@ -371,7 +371,7 @@ b2c setup skills [SKILLSET] | Argument | Description | Default | | ---------- | ---------------------------------------- | ---------------------- | -| `SKILLSET` | Skill set to install: `b2c` or `b2c-cli` | Prompted interactively | +| `SKILLSET` | Skill set to install: `b2c`, `b2c-cli`, `storefront-next`, or `cap-dev` | Prompted interactively | ### Flags @@ -411,6 +411,7 @@ b2c setup skills # List available skills in a skillset b2c setup skills b2c --list b2c setup skills b2c-cli --list +b2c setup skills storefront-next --list # Install b2c skills to Cursor (project scope) b2c setup skills b2c --ide cursor @@ -447,7 +448,7 @@ b2c setup skills b2c --list --json When run without `--force`, the command provides an interactive experience: -1. Prompts you to select skill set(s) (if not provided as argument) - you can select both `b2c` and `b2c-cli` +1. Prompts you to select skill set(s) (if not provided as argument) - you can select multiple sets 2. Downloads skills from the latest release (or specified version) 3. Auto-detects installed IDEs 4. Prompts you to select target IDEs @@ -465,6 +466,7 @@ For Claude Code users, we recommend using the plugin marketplace for automatic u claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling claude plugin install b2c-cli claude plugin install b2c +claude plugin install storefront-next ``` The marketplace provides: @@ -477,10 +479,12 @@ Use `--ide manual` if you prefer manual installation, or `--ide agentforce-vibes ### Skill Sets -| Skill Set | Description | -| --------- | ----------------------------------------------- | -| `b2c` | B2C Commerce development patterns and practices | -| `b2c-cli` | B2C CLI commands and operations | +| Skill Set | Description | +| ------------------ | -------------------------------------------------------------- | +| `b2c` | B2C Commerce development patterns and practices | +| `b2c-cli` | B2C CLI commands and operations | +| `storefront-next` | Storefront Next development — routing, components, deployment | +| `cap-dev` | Commerce App Package scaffolding, validation, and submission | ### Output diff --git a/docs/guide/agent-skills.md b/docs/guide/agent-skills.md index 15930ea2..8e771bc4 100644 --- a/docs/guide/agent-skills.md +++ b/docs/guide/agent-skills.md @@ -17,6 +17,7 @@ claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling # Use --scope project to install for current project only claude plugin install b2c-cli claude plugin install b2c +claude plugin install storefront-next ``` ```text [Copilot (VS Code)] @@ -30,6 +31,7 @@ Then enter: copilot plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling copilot plugin install b2c-cli@b2c-developer-tooling copilot plugin install b2c@b2c-developer-tooling +copilot plugin install storefront-next@b2c-developer-tooling ``` ```bash [Codex] @@ -68,6 +70,10 @@ npx @salesforce/b2c-cli setup skills b2c B2C Commerce development patterns — controllers, ISML, forms, localization, logging, metadata, web services, custom job steps, Page Designer, Business Manager extensions, Custom APIs + + storefront-next + Storefront Next development — project setup, routing, data fetching, components, Page Designer, authentication, i18n, extensions, testing, and deployment to Managed Runtime + b2c-dx-mcp Automatic project type detection and B2C Commerce workflows for your AI assistant. See MCP Installation @@ -90,12 +96,14 @@ Install plugins at your preferred scope: ```bash [User Scope (default)] claude plugin install b2c-cli claude plugin install b2c +claude plugin install storefront-next claude plugin install b2c-dx-mcp ``` ```bash [Project Scope] claude plugin install b2c-cli --scope project claude plugin install b2c --scope project +claude plugin install storefront-next --scope project claude plugin install b2c-dx-mcp --scope project ``` @@ -107,6 +115,7 @@ Verify, update, or uninstall: claude plugin list claude plugin marketplace update claude plugin update b2c-cli@b2c-developer-tooling +claude plugin update storefront-next@b2c-developer-tooling claude plugin uninstall b2c-cli@b2c-developer-tooling claude plugin marketplace remove b2c-developer-tooling ``` @@ -145,6 +154,7 @@ List available skills: ```bash b2c setup skills b2c --list b2c setup skills b2c-cli --list +b2c setup skills storefront-next --list ``` Install to specific IDEs: @@ -240,3 +250,7 @@ Once installed, ask your AI assistant: - "Help me create a Custom API for loyalty information" - "Add logging to my checkout controller" - "Create an HTTP service to call the payment gateway API" +- "Set up a new Storefront Next project" +- "Add a new route with a loader to my Storefront Next app" +- "Deploy my Storefront Next storefront to Managed Runtime" +- "Add Page Designer support to my storefront component" diff --git a/packages/b2c-cli/src/commands/setup/skills.ts b/packages/b2c-cli/src/commands/setup/skills.ts index 33823a72..9b808121 100644 --- a/packages/b2c-cli/src/commands/setup/skills.ts +++ b/packages/b2c-cli/src/commands/setup/skills.ts @@ -62,7 +62,7 @@ interface SetupSkillsResponse { export default class SetupSkills extends BaseCommand { static args = { skillset: Args.string({ - description: 'Skill set to install: b2c, b2c-cli, or cap-dev', + description: 'Skill set to install: b2c, b2c-cli, storefront-next, or cap-dev', options: ALL_SKILL_SETS, }), }; @@ -77,6 +77,7 @@ export default class SetupSkills extends BaseCommand { static examples = [ '<%= config.bin %> <%= command.id %> b2c', '<%= config.bin %> <%= command.id %> b2c-cli --ide cursor --global', + '<%= config.bin %> <%= command.id %> storefront-next --ide cursor', '<%= config.bin %> <%= command.id %> cap-dev --ide claude-code --global', '<%= config.bin %> <%= command.id %> b2c --list', '<%= config.bin %> <%= command.id %> b2c-cli --skill b2c-code --skill b2c-webdav --ide cursor', @@ -133,7 +134,7 @@ export default class SetupSkills extends BaseCommand { this.error( t( 'commands.setup.skills.skillsetRequired', - 'Skillset argument required in non-interactive mode. Specify b2c, b2c-cli, or cap-dev.', + 'Skillset argument required in non-interactive mode. Specify b2c, b2c-cli, storefront-next, or cap-dev.', ), ); } else { @@ -269,7 +270,8 @@ export default class SetupSkills extends BaseCommand { 'Note: For Claude Code, we recommend using the plugin marketplace for automatic updates:\n' + ' claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling\n' + ' claude plugin install b2c-cli\n' + - ' claude plugin install b2c\n\n' + + ' claude plugin install b2c\n' + + ' claude plugin install storefront-next\n\n' + 'Use --ide manual for manual installation to the same paths.', ), ); diff --git a/packages/b2c-cli/src/i18n/locales/en.ts b/packages/b2c-cli/src/i18n/locales/en.ts index 2ced47e6..1936e6fb 100644 --- a/packages/b2c-cli/src/i18n/locales/en.ts +++ b/packages/b2c-cli/src/i18n/locales/en.ts @@ -179,14 +179,16 @@ export const en = { 'Note: For Claude Code, we recommend using the plugin marketplace for automatic updates:\n' + ' claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling\n' + ' claude plugin install b2c-cli\n' + - ' claude plugin install b2c\n\n' + + ' claude plugin install b2c\n' + + ' claude plugin install storefront-next\n\n' + 'Use --ide manual for manual installation to the same paths.', preview: 'Installing {{count}} skills to {{ides}} ({{scope}})', cancelled: 'Installation cancelled.', installed: 'Successfully installed {{count}} skill(s):', skippedCount: 'Skipped {{count}} skill(s):', errorsCount: 'Failed to install {{count}} skill(s):', - skillsetRequired: 'Skillset argument required in non-interactive mode. Specify b2c or b2c-cli.', + skillsetRequired: + 'Skillset argument required in non-interactive mode. Specify b2c, b2c-cli, storefront-next, or cap-dev.', selectSkillset: 'Select skill set(s) to install:', noSkillsetsSelected: 'No skill sets selected.', selectIdes: 'Select target IDEs:', diff --git a/packages/b2c-tooling-sdk/src/skills/github.ts b/packages/b2c-tooling-sdk/src/skills/github.ts index 5f50ac3d..58121b58 100644 --- a/packages/b2c-tooling-sdk/src/skills/github.ts +++ b/packages/b2c-tooling-sdk/src/skills/github.ts @@ -135,9 +135,11 @@ function parseRelease(release: { }): ReleaseInfo { const b2cSource = getSkillSource('b2c'); const b2cCliSource = getSkillSource('b2c-cli'); + const sfNextSource = getSkillSource('storefront-next'); const b2cAsset = release.assets.find((a) => a.name === b2cSource.assetName); const b2cCliAsset = release.assets.find((a) => a.name === b2cCliSource.assetName); + const sfNextAsset = release.assets.find((a) => a.name === sfNextSource.assetName); const versionMatch = release.tag_name.match(/@(\d+\.\d+\.\d+.*)$/); const version = versionMatch ? versionMatch[1] : release.tag_name.replace(/^v/, ''); @@ -148,6 +150,7 @@ function parseRelease(release: { publishedAt: release.published_at, b2cSkillsAssetUrl: b2cAsset?.browser_download_url ?? null, b2cCliSkillsAssetUrl: b2cCliAsset?.browser_download_url ?? null, + storefrontNextSkillsAssetUrl: sfNextAsset?.browser_download_url ?? null, }; } @@ -228,7 +231,7 @@ export async function listReleases(limit: number = 10): Promise { return data .filter((r) => r.tag_name.startsWith('b2c-agent-plugins@')) .map(parseRelease) - .filter((r) => r.b2cSkillsAssetUrl || r.b2cCliSkillsAssetUrl); + .filter((r) => r.b2cSkillsAssetUrl || r.b2cCliSkillsAssetUrl || r.storefrontNextSkillsAssetUrl); } /** diff --git a/packages/b2c-tooling-sdk/src/skills/sources.ts b/packages/b2c-tooling-sdk/src/skills/sources.ts index 50a5693b..34fc5adb 100644 --- a/packages/b2c-tooling-sdk/src/skills/sources.ts +++ b/packages/b2c-tooling-sdk/src/skills/sources.ts @@ -28,6 +28,14 @@ export const SKILL_SOURCES: Record = { assetName: 'b2c-cli-skills.zip', tagPattern: pluginsTag, }, + 'storefront-next': { + id: 'storefront-next', + displayName: 'Storefront Next development skills', + type: 'release-artifact', + repo: 'SalesforceCommerceCloud/b2c-developer-tooling', + assetName: 'storefront-next-skills.zip', + tagPattern: pluginsTag, + }, 'cap-dev': { id: 'cap-dev', displayName: 'Commerce Apps development skills', diff --git a/packages/b2c-tooling-sdk/src/skills/types.ts b/packages/b2c-tooling-sdk/src/skills/types.ts index 700b930e..8029c392 100644 --- a/packages/b2c-tooling-sdk/src/skills/types.ts +++ b/packages/b2c-tooling-sdk/src/skills/types.ts @@ -20,7 +20,7 @@ export type IdeType = /** * Skill set categories matching the plugins directory structure. */ -export type SkillSet = 'b2c' | 'b2c-cli' | 'cap-dev'; +export type SkillSet = 'b2c' | 'b2c-cli' | 'cap-dev' | 'storefront-next'; /** * Configuration for a skill source — defines how to fetch skills from a particular repository. @@ -92,6 +92,8 @@ export interface ReleaseInfo { b2cSkillsAssetUrl: string | null; /** Download URL for b2c-cli-skills.zip asset, or null if not present */ b2cCliSkillsAssetUrl: string | null; + /** Download URL for storefront-next-skills.zip asset, or null if not present */ + storefrontNextSkillsAssetUrl: string | null; } /** diff --git a/packages/b2c-tooling-sdk/test/skills/sources.test.ts b/packages/b2c-tooling-sdk/test/skills/sources.test.ts index 3b5f9324..d9e9c733 100644 --- a/packages/b2c-tooling-sdk/test/skills/sources.test.ts +++ b/packages/b2c-tooling-sdk/test/skills/sources.test.ts @@ -49,7 +49,8 @@ describe('skill sources', () => { expect(ALL_SKILL_SETS).to.include('b2c'); expect(ALL_SKILL_SETS).to.include('b2c-cli'); expect(ALL_SKILL_SETS).to.include('cap-dev'); - expect(ALL_SKILL_SETS).to.have.lengthOf(3); + expect(ALL_SKILL_SETS).to.include('storefront-next'); + expect(ALL_SKILL_SETS).to.have.lengthOf(4); }); }); diff --git a/scripts/sync-plugin-versions.mjs b/scripts/sync-plugin-versions.mjs index 2bc67385..5360211b 100644 --- a/scripts/sync-plugin-versions.mjs +++ b/scripts/sync-plugin-versions.mjs @@ -34,7 +34,7 @@ if (!version) { // b2c-dx-mcp is NOT part of b2c-agent-plugins — it tracks @salesforce/b2c-dx-mcp separately. const marketplacePath = join(repoRoot, '.claude-plugin/marketplace.json'); const marketplace = readJson(marketplacePath); -const claudeTargets = new Set(['b2c-cli', 'b2c']); +const claudeTargets = new Set(['b2c-cli', 'b2c', 'storefront-next']); for (const plugin of marketplace.plugins) { if (claudeTargets.has(plugin.name)) { plugin.version = version; @@ -46,6 +46,7 @@ writeJson(marketplacePath, marketplace); const codexTargets = [ 'skills/b2c-cli/.codex-plugin/plugin.json', 'skills/b2c/.codex-plugin/plugin.json', + 'skills/storefront-next/.codex-plugin/plugin.json', ]; for (const rel of codexTargets) { const path = join(repoRoot, rel); diff --git a/skills/storefront-next/.codex-plugin/plugin.json b/skills/storefront-next/.codex-plugin/plugin.json new file mode 100644 index 00000000..af2a33b8 --- /dev/null +++ b/skills/storefront-next/.codex-plugin/plugin.json @@ -0,0 +1,39 @@ +{ + "name": "storefront-next", + "version": "1.1.2", + "description": "Storefront Next development skills for building React 19 storefronts on Salesforce B2C Commerce.", + "author": { + "name": "Salesforce" + }, + "homepage": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "repository": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling", + "license": "Apache-2.0", + "keywords": [ + "salesforce", + "storefront-next", + "react", + "b2c-commerce", + "commerce-cloud" + ], + "skills": "./skills/", + "interface": { + "displayName": "Storefront Next", + "shortDescription": "Build Salesforce B2C Storefront Next projects with AI assistance.", + "longDescription": "Skills for developing React 19 storefronts on Salesforce B2C Commerce — routing, data fetching, components, Page Designer, authentication, i18n, extensions, testing, and deployment to Managed Runtime.", + "developerName": "Salesforce", + "category": "Productivity", + "capabilities": [ + "Read" + ], + "logo": "./assets/logo.svg", + "composerIcon": "./assets/logo.svg", + "brandColor": "#0D9DDA", + "websiteURL": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "defaultPrompt": [ + "Set up a new Storefront Next project", + "Add a new route with data fetching to my storefront", + "Deploy my storefront to Managed Runtime", + "Add Page Designer support to my storefront component" + ] + } +} diff --git a/skills/storefront-next/README.md b/skills/storefront-next/README.md new file mode 100644 index 00000000..b07d968d --- /dev/null +++ b/skills/storefront-next/README.md @@ -0,0 +1,48 @@ +# storefront-next + +Agent skills for building Salesforce B2C Storefront Next projects — React 19 storefronts with routing, data fetching, Page Designer, authentication, i18n, extensions, and deployment to Managed Runtime. + +Part of the [B2C Developer Tooling](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling) marketplace. + +## Installation + +```bash +# Claude Code +claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling +claude plugin install storefront-next@b2c-developer-tooling + +# GitHub Copilot CLI +copilot plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling +copilot plugin install storefront-next@b2c-developer-tooling +``` + +**VS Code (GitHub Copilot):** Command Palette → **Chat: Install Plugin From Source** → enter the repo `SalesforceCommerceCloud/b2c-developer-tooling`. + +**Codex:** open the repo as a workspace, restart Codex, then install from the **B2C Developer Tooling** marketplace in the plugin directory. + +For file-copy install to any supported IDE, use `b2c setup skills storefront-next`. See the [install guide](https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/agent-skills) for details. + +## What's included + +Skills covering the full Storefront Next development lifecycle: + +- **`sfnext-project-setup`** — project creation, environment configuration, project structure +- **`sfnext-routing`** — React Router 7 file-based routing with flat-routes conventions +- **`sfnext-data-fetching`** — server-side loaders, actions, useScapiFetcher +- **`sfnext-components`** — UI components, createPage HOC, shadcn/ui, Tailwind CSS v4 +- **`sfnext-configuration`** — config.server.ts, environment variables, multi-site setup +- **`sfnext-page-designer`** — Page Designer integration with React decorators and component registry +- **`sfnext-extensions`** — extension system, target points, extension routes +- **`sfnext-authentication`** — split-cookie SLAS architecture, auth middleware, session management +- **`sfnext-i18n`** — internationalization with i18next, dual-instance server/client, namespaces +- **`sfnext-state-management`** — React context, Zustand stores, basket provider +- **`sfnext-testing`** — Vitest unit tests, Storybook stories, interaction and accessibility testing +- **`sfnext-performance`** — bundle size limits, DynamicImage, parallel fetching, Lighthouse optimization +- **`sfnext-deployment`** — build and deploy to Managed Runtime (MRT), cartridge deployment +- **`sfnext-hybrid-storefronts`** — hybrid setup with SFRA/SiteGenesis, gradual migration, session bridging + +See [`skills/`](./skills/) for the full list. + +## License + +Apache-2.0. See the [repo LICENSE](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/blob/main/LICENSE.txt). diff --git a/skills/storefront-next/assets/logo.svg b/skills/storefront-next/assets/logo.svg new file mode 100644 index 00000000..b2f3856b --- /dev/null +++ b/skills/storefront-next/assets/logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skills/storefront-next/skills/sfnext-authentication/SKILL.md b/skills/storefront-next/skills/sfnext-authentication/SKILL.md new file mode 100644 index 00000000..f6545d4a --- /dev/null +++ b/skills/storefront-next/skills/sfnext-authentication/SKILL.md @@ -0,0 +1,120 @@ +--- +name: sfnext-authentication +description: Implement authentication in Storefront Next using split-cookie architecture, SLAS tokens, and auth middleware. Use when accessing user identity in loaders, detecting guest vs registered users, using getAuth or useAuth, or understanding session management and token refresh. +--- + +# Authentication Skill + +This skill covers Storefront Next's split-cookie authentication architecture with SLAS (Shopper Login and API Access Service). + +## Overview + +Storefront Next uses a split-cookie architecture separating server and client auth concerns: + +- **Server middleware** (`auth.server.ts`) — Manages SLAS tokens, writes cookies, handles token refresh +- **React Context** (`AuthProvider`) — Provides public session data (non-sensitive fields) to components via `useAuth()` + +## Cookie Design + +| Cookie | Purpose | User Type | Expiry | HttpOnly | +|--------|---------|-----------|--------|----------| +| `cc-nx-g` | Guest refresh token | Guest | 30 days | No | +| `cc-nx` | Registered refresh token | Registered | 90 days | No | +| `cc-at` | Access token | Both | 30 min | No | +| `usid` | User session ID | Both | Matches refresh | No | +| `customerId` | Customer ID | Registered | Matches refresh | No | + +**Key points:** +- Only ONE refresh token exists at a time (guest OR registered, never both) +- User type is derived from which refresh token is present +- Cookies are auto-namespaced with `siteId` +- Tokens auto-refresh when expired + +## Usage in Loaders/Actions + +```typescript +import { getAuth } from '@/middlewares/auth.server'; + +export function loader({ context }: LoaderFunctionArgs) { + const auth = getAuth(context); + + const { accessToken, customerId, userType } = auth; + const isGuest = userType === 'guest'; + const isRegistered = userType === 'registered'; + + return { isGuest, customerId }; +} +``` + +## Usage in Components + +```typescript +import { useAuth } from '@/providers/auth'; + +export function MyComponent() { + const auth = useAuth(); + + if (auth?.userType === 'guest') { + return ; + } + + return
Welcome, customer {auth?.customerId}
; +} +``` + +## Common Patterns + +### Protected Routes + +```typescript +export function loader({ context }: LoaderFunctionArgs) { + const auth = getAuth(context); + + if (auth.userType === 'guest') { + throw redirect('/login'); + } + + const clients = createApiClients(context); + return { + orders: clients.shopperOrders.getOrders({ + params: { path: { customerId: auth.customerId } } + }).then(({ data }) => data), + }; +} +``` + +### Conditional Data Loading + +```typescript +export function loader({ context }: LoaderFunctionArgs) { + const auth = getAuth(context); + const clients = createApiClients(context); + + const base = { + products: clients.shopperProducts.getProducts({...}).then(({ data }) => data), + }; + + if (auth.userType === 'registered') { + return { + ...base, + wishlist: clients.shopperCustomers.getWishlist({...}).then(({ data }) => data), + }; + } + + return base; +} +``` + +## Troubleshooting + +| Issue | Cause | Solution | +|-------|-------|----------| +| `auth` is undefined | Missing auth middleware | Ensure `auth.server.ts` middleware is configured | +| Always guest | Refresh token expired | Check cookie expiry; SLAS auto-refreshes | +| Token errors in SCAPI | Stale access token | Tokens auto-refresh; check SLAS client configuration | + +## Related Skills + +- `storefront-next:sfnext-data-fetching` - Using auth context in loader functions +- `storefront-next:sfnext-configuration` - SLAS client configuration +- `storefront-next:sfnext-hybrid-storefronts` - Session bridging with SFRA diff --git a/skills/storefront-next/skills/sfnext-components/SKILL.md b/skills/storefront-next/skills/sfnext-components/SKILL.md new file mode 100644 index 00000000..c33d6879 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-components/SKILL.md @@ -0,0 +1,191 @@ +--- +name: sfnext-components +description: Build UI components in Storefront Next using createPage HOC, Suspense/Await patterns, shadcn/ui, and Tailwind CSS v4. Use when creating page components, adding Suspense boundaries, integrating shadcn/ui, styling with Tailwind, or organizing component files. Covers server vs client rendering patterns and the cn() utility. +--- + +# Components Skill + +This skill covers component development patterns in Storefront Next — createPage HOC, Suspense boundaries, shadcn/ui integration, and Tailwind CSS styling. + +## Page Component Pattern + +Most routes export a default function component that receives `loaderData` as props: + +```typescript +import { Suspense } from 'react'; +import { Await } from 'react-router'; +import { SeoMeta } from '@/components/seo-meta'; + +type ProductPageData = { + product: Promise; + reviews: Promise; +}; + +export default function ProductPage({ loaderData }: { loaderData: ProductPageData }) { + return ( + <> + + }> + + {(product) => } + + + }> + + {(reviews) => } + + + + ); +} +``` + +### createPage HOC (Optional) + +For pages needing standardized Suspense wrappers and page key management: + +```typescript +import { createPage } from '@/components/create-page'; + +export default createPage({ + component: ProductView, + fallback: , +}); +``` + +## Suspense Boundaries and Code Splitting + +Use `` + `` for streaming loader data, and `lazy()` for code-splitting heavy components: + +```typescript +import { lazy, Suspense } from 'react'; + +// Code-split a heavy component +const CustomerReviewsSection = lazy(() => + import('@/components/customer-reviews-section/customer-reviews-section') +); + +export default function ProductPage({ loaderData }: { loaderData: ProductPageData }) { + return ( + <> + {/* Stream loader data */} + }> + + {(product) => } + + + + {/* Code-split component */} + }> + + + + ); +} +``` + +## shadcn/ui Components + +shadcn/ui provides pre-built accessible components. Add them via CLI: + +```bash +npx shadcn@latest add button +npx shadcn@latest add card +npx shadcn@latest add dialog +``` + +**Rules:** + +- Add components via CLI only (do not manually create files in `src/components/ui/`) +- `src/components/ui/` components are copied into your project and can be customized directly +- Keep app/domain components outside `src/components/ui/`: + +```typescript +// Optional wrapper for app-specific styling +import { Button } from '@/components/ui/button'; + +export function PrimaryButton(props: React.ComponentProps) { + return + +``` + +### Responsive Design + +```typescript +
+ {products.map(p => )} +
+``` + +## File Organization + +``` +src/components/product-tile/ +├── index.tsx # Component implementation +├── index.test.tsx # Vitest unit tests +└── stories/ + ├── index.stories.tsx # Storybook stories + └── __snapshots__/ # Storybook snapshots (optional) + +src/components/product-skeleton/ +├── index.tsx # Skeleton component (separate from main) +├── index.test.tsx +└── stories/ + └── index.stories.tsx +``` + +## Best Practices + +1. **Export default function components** — Receive `loaderData` as props +2. **Granular Suspense boundaries** — Show content progressively as data resolves +3. **Use `lazy()` for heavy components** — Code-split below-the-fold or conditional UI +4. **Reusable skeleton components** — Consistent loading states +5. **Colocate tests and stories** — Keep test files next to source files +6. **TypeScript interfaces** — Define proper types for all props + +## Related Skills + +- `storefront-next:sfnext-data-fetching` - Loader patterns that feed data to components +- `storefront-next:sfnext-testing` - Writing Vitest tests and Storybook stories +- `storefront-next:sfnext-page-designer` - Page Designer component integration +- `storefront-next:sfnext-i18n` - Translating component text diff --git a/skills/storefront-next/skills/sfnext-configuration/SKILL.md b/skills/storefront-next/skills/sfnext-configuration/SKILL.md new file mode 100644 index 00000000..81f4bd4c --- /dev/null +++ b/skills/storefront-next/skills/sfnext-configuration/SKILL.md @@ -0,0 +1,160 @@ +--- +name: sfnext-configuration +description: Manage Storefront Next application configuration using config.server.ts, schema types, environment variables, and multi-site setup. Use when editing config.server.ts, adding PUBLIC__ environment variables, using useConfig or getConfig, or configuring multiple sites. NOT for initial project creation — see sfnext-project-setup. +--- + +# Configuration Skill + +This skill covers the Storefront Next configuration system — centralized in `config.server.ts` with environment variable overrides. + +## Overview + +All configuration is centralized in `config.server.ts` with typed defaults. Environment variables (via `.env` files or MRT settings) override these defaults. The system provides type-safe access with automatic parsing and validation. + +## Adding Configuration + +### 1. Define the type in `src/types/config.ts` + +```typescript +export type Config = { + app: { + myFeature: { + enabled: boolean; + maxItems: number; + }; + }; +}; +``` + +### 2. Set defaults in `config.server.ts` + +```typescript +export default defineConfig({ + app: { + myFeature: { + enabled: false, + maxItems: 10, + }, + }, +}); +``` + +### 3. Override via environment variables + +```bash +PUBLIC__app__myFeature__enabled=true +PUBLIC__app__myFeature__maxItems=20 +``` + +## Accessing Configuration + +### In React Components + +```typescript +import { useConfig } from '@salesforce/storefront-next-runtime/config'; + +export function MyComponent() { + const config = useConfig(); + + if (config.myFeature.enabled) { + const maxItems = config.myFeature.maxItems; + // Feature code + } +} +``` + +### In Server Loaders/Actions + +```typescript +import { getConfig } from '@salesforce/storefront-next-runtime/config'; + +export function loader({context}: LoaderFunctionArgs) { + const config = getConfig(context); // context is required on server + + if (config.myFeature.enabled) { + // Loader code + } +} +``` + +### In Browser Code (Non-Route Modules) + +```typescript +import { getConfig } from '@salesforce/storefront-next-runtime/config'; + +export function getFeatureFlag() { + const config = getConfig(); // No context needed in browser (uses window.__APP_CONFIG__) + return config.myFeature.enabled; +} +``` + +**Note:** `getConfig()` and `useConfig()` return `AppConfig` — the `app` section of the full config. Access properties directly (e.g., `config.myFeature.enabled`) without the `app` prefix. + +## Environment Variable Rules + +```bash +# Pattern: PUBLIC__app__{path}__{to}__{property}=value +PUBLIC__app__commerce__api__clientId=abc123 +# Maps to config.app.commerce.api.clientId +# Accessed as config.commerce.api.clientId +``` + +| Rule | Detail | +| ----------------- | ---------------------------------------------- | +| `PUBLIC__` prefix | Exposed to browser (client-safe) | +| No prefix | Server-only (secrets) | +| `__` separator | Navigate nested paths | +| Auto-parsing | Numbers, booleans, JSON parsed automatically | +| Validation | Paths must exist in `config.server.ts` | +| Depth limit | Maximum 10 levels | +| MRT limits | Names max 512 chars; total `PUBLIC__` max 32KB | + +## Multi-Site Configuration + +```bash +PUBLIC__app__commerce__sites='[ + { + "id": "RefArchGlobal", + "defaultLocale": "en-US", + "defaultCurrency": "USD", + "supportedLocales": [ + {"id": "en-US", "preferredCurrency": "USD"}, + {"id": "de-DE", "preferredCurrency": "EUR"} + ], + "supportedCurrencies": ["USD", "EUR"] + } +]' +``` + +```typescript +const config = getConfig(context); +const currentSite = config.commerce.sites[0]; +const locale = currentSite.defaultLocale; // "en-US" +const currency = currentSite.defaultCurrency; // "USD" +``` + +## Security + +```bash +# Client-safe (PUBLIC__ prefix) +PUBLIC__app__commerce__api__clientId=abc123 + +# Server-only (no prefix — never sent to client) +COMMERCE_API_SLAS_SECRET=your-secret +``` + +Read server-only secrets directly from `process.env` — never add them to the config system. + +## Common Pitfalls + +| Pitfall | Problem | Solution | +| ----------------- | ------------------------------------------ | -------------------------------------------------- | +| Missing `context` | `getConfig()` returns undefined in loaders | Use `getConfig(context)` on server | +| Typo in env var | Variable silently ignored | Validation catches paths not in `config.server.ts` | +| Exposing secrets | Sensitive data in browser | Use no-prefix variables; access via `process.env` | + +## Related Skills + +- `storefront-next:sfnext-project-setup` - Initial environment setup and `.env` configuration +- `storefront-next:sfnext-data-fetching` - Using config in loader functions +- `storefront-next:sfnext-deployment` - MRT environment variable configuration diff --git a/skills/storefront-next/skills/sfnext-data-fetching/SKILL.md b/skills/storefront-next/skills/sfnext-data-fetching/SKILL.md new file mode 100644 index 00000000..326cc2d9 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-data-fetching/SKILL.md @@ -0,0 +1,188 @@ +--- +name: sfnext-data-fetching +description: Implement server-side data fetching in Storefront Next using loaders, actions, and useScapiFetcher. Use when writing loader functions, making SCAPI calls, handling form submissions, or implementing interactive data fetching. Covers synchronous loaders, streaming patterns, createApiClients, and parallel requests. NOT for client-side Zustand state — see sfnext-state-management. +--- + +# Data Fetching Skill + +This skill covers server-side data fetching patterns in Storefront Next — loaders, actions, and the useScapiFetcher hook. + +## Overview + +Storefront Next mandates **server-only data loading**. All SCAPI requests execute on the MRT server, never in the browser. Three mechanisms exist: + +| Mechanism | When It Runs | Use Case | +|-----------|-------------|----------| +| `loader` | Route navigation | Initial page data | +| `action` | Form submission | Mutations (add to cart, update profile) | +| `useScapiFetcher` | User interaction | On-demand fetching (search suggestions, infinite scroll) | + +## Loader Patterns + +Loaders can be **synchronous** (returning promises for streaming) or **async** (awaiting critical data). Choose based on what the page needs: + +- **Sync loader** — Returns promises directly. Enables streaming SSR: the shell renders immediately while data streams in. Best when all data can render progressively. +- **Async loader** — Awaits critical data before rendering. Use when data is required for SEO or the page shell (e.g., category name in breadcrumbs). Non-critical data can still be returned as promises for streaming. + +```typescript +// Sync — full streaming (all data renders progressively) +export function loader({ params, context }: LoaderFunctionArgs): ProductPageData { + const clients = createApiClients(context); + return { + product: clients.shopperProducts.getProduct({ + params: { path: { id: params.productId } } + }).then(({ data }) => data), + reviews: clients.shopperProducts.getReviews({ + params: { path: { id: params.productId } } + }).then(({ data }) => data), + }; +} + +// Async — await critical data, stream the rest (mixed strategy) +export async function loader({ params, context }: LoaderFunctionArgs): Promise { + const clients = createApiClients(context); + + // Await critical data needed for page shell/SEO + const category = await clients.shopperProducts.getCategory({ + params: { path: { id: params.categoryId } } + }).then(({ data }) => data); + + return { + category, // Resolved immediately + products: clients.shopperSearch.productSearch({ + params: { query: { q: '', refine: { cgid: params.categoryId } } } + }).then(({ data }) => data), // Streamed + }; +} +``` + +## When to Use Each Pattern + +| Pattern | When | Example | +|---------|------|---------| +| Sync (full streaming) | All data can render progressively | Product page with reviews | +| Async (await critical) | SEO-critical data needed for page shell | Category page (needs category name) | +| Mixed | Some data critical, some deferrable | Category name (await) + product grid (stream) | + +See [Loader Patterns Reference](references/LOADER-PATTERNS.md) for more patterns and data flow diagrams. + +## Action Functions + +Handle mutations (form submissions, cart updates): + +```typescript +import { data, redirect } from 'react-router'; + +export async function action({ request, context }: ActionFunctionArgs) { + const formData = await request.formData(); + const productId = formData.get('productId') as string; + + const clients = createApiClients(context); + + try { + await clients.shopperBasketsV2.addItemToBasket({ + params: { + path: { basketId }, + body: { productId, quantity: 1 }, + }, + }); + return data({ success: true }); + } catch (error) { + return data({ success: false, error: error.message }, { status: 400 }); + } +} +``` + +## useScapiFetcher — Interactive Data Fetching + +For on-demand data fetching triggered by user interactions (after page load): + +```typescript +import { useScapiFetcher } from '@/hooks/use-scapi-fetcher'; + +export function useSearchSuggestions({ q, limit, currency }) { + const parameters = useMemo( + () => ({ params: { query: { q, limit, currency } } }), + [q, limit, currency] + ); + + const fetcher = useScapiFetcher( + 'shopperSearch', + 'getSearchSuggestions', + parameters + ); + + const refetch = useCallback(async () => { + await fetcher.load(); + }, [fetcher]); + + return { + data: fetcher.data, + isLoading: fetcher.state === 'loading', + refetch, + }; +} +``` + +See [SCAPI Fetcher Reference](references/SCAPI-FETCHER.md) for the complete useScapiFetcher API. + +## API Client Usage + +Always use `createApiClients(context)` in loaders and actions: + +```typescript +import { createApiClients } from '@/lib/api-clients'; + +export function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + + clients.shopperProducts.getProduct({...}); + clients.shopperCustomers.getCustomer({...}); + clients.shopperBasketsV2.getBasket({...}); + clients.shopperSearch.productSearch({...}); + clients.shopperOrders.getOrder({...}); +} +``` + +## Parallel vs Sequential Requests + +```typescript +// GOOD — Parallel requests (all start simultaneously) +export function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + return { + product: clients.shopperProducts.getProduct({...}).then(({ data }) => data), + reviews: clients.shopperProducts.getReviews({...}).then(({ data }) => data), + recommendations: clients.shopperProducts.getRecommendations({...}).then(({ data }) => data), + }; +} + +// AVOID — Sequential awaits of independent requests (unnecessarily slow) +export async function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + const product = await clients.shopperProducts.getProduct({...}); // Waits... + const reviews = await clients.shopperProducts.getReviews({...}); // Then waits again + return { product, reviews }; +} +``` + +## Common Pitfalls + +| Pitfall | Problem | Solution | +|---------|---------|----------| +| Awaiting all data | Blocks page transition unnecessarily | Only await SEO-critical data; stream the rest | +| Client loaders | Not permitted in Storefront Next | Use server `loader` or `useScapiFetcher` | +| Sequential `await` | Slow data loading | Return promises in parallel | +| Missing `context` in `getConfig()` | Config unavailable | Pass `context` in server loaders: `getConfig(context)` | + +## Related Skills + +- `storefront-next:sfnext-routing` - Route file conventions and module exports +- `storefront-next:sfnext-components` - Rendering loader data with createPage and Suspense +- `storefront-next:sfnext-state-management` - Client-side Zustand stores (NOT data fetching) +- `storefront-next:sfnext-authentication` - Auth context in loaders + +## Reference Documentation + +- [Loader Patterns Reference](references/LOADER-PATTERNS.md) - Data flow diagrams and advanced patterns +- [SCAPI Fetcher Reference](references/SCAPI-FETCHER.md) - Complete useScapiFetcher API and examples diff --git a/skills/storefront-next/skills/sfnext-data-fetching/references/LOADER-PATTERNS.md b/skills/storefront-next/skills/sfnext-data-fetching/references/LOADER-PATTERNS.md new file mode 100644 index 00000000..f77a2909 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-data-fetching/references/LOADER-PATTERNS.md @@ -0,0 +1,102 @@ +# Loader Patterns Reference + +## Data Flow + +### Initial Page Load (SSR) + +``` +Browser request → MRT Server + ↓ + loader() runs on server + ↓ + SCAPI requests on MRT + ↓ + HTML response streamed → Browser +``` + +### Subsequent Navigation (SPA) + +``` +User clicks link → React Router intercepts + ↓ + Browser fetches from server + ↓ + MRT Server runs same loader() + ↓ + SCAPI requests on MRT + ↓ + JSON response → Browser + ↓ + React updates DOM +``` + +The loader code is identical for SSR and SPA navigation — only the response format differs (HTML vs JSON). + +## Decision Tree: Which Pattern to Use + +``` +Need data for page render? +├── Critical for SEO / above-the-fold? +│ └── Use awaited data (async loader with await) +├── Non-critical / below-the-fold? +│ └── Use deferred data (sync loader, return promises) +└── Mix of both? + └── Use mixed strategy (await critical, stream rest) + +Need to handle mutations? +└── Use action function + +Need data after page load (user interaction)? +└── Use useScapiFetcher +``` + +## Pattern: Multiple API Calls with Dependencies + +When one API call depends on another's result: + +```typescript +export async function loader({ params, context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + + // First call — must resolve before dependent calls + const category = await clients.shopperProducts.getCategory({ + params: { path: { id: params.categoryId } } + }).then(({ data }) => data); + + // Dependent calls — can run in parallel with each other + return { + category, + products: clients.shopperSearch.productSearch({ + params: { query: { refine: `cgid=${category.id}` } } + }).then(({ data }) => data), + refinements: clients.shopperSearch.getSearchSuggestions({ + params: { query: { q: category.name } } + }).then(({ data }) => data), + }; +} +``` + +## Pattern: Conditional Data Loading + +```typescript +export function loader({ params, context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + const auth = getAuth(context); + + const base = { + product: clients.shopperProducts.getProduct({ + params: { path: { id: params.productId } } + }).then(({ data }) => data), + }; + + // Only fetch wishlist for registered users + if (auth.userType === 'registered') { + return { + ...base, + wishlist: clients.shopperCustomers.getWishlist({...}).then(({ data }) => data), + }; + } + + return base; +} +``` diff --git a/skills/storefront-next/skills/sfnext-data-fetching/references/SCAPI-FETCHER.md b/skills/storefront-next/skills/sfnext-data-fetching/references/SCAPI-FETCHER.md new file mode 100644 index 00000000..42ba878f --- /dev/null +++ b/skills/storefront-next/skills/sfnext-data-fetching/references/SCAPI-FETCHER.md @@ -0,0 +1,93 @@ +# SCAPI Fetcher Reference + +## How useScapiFetcher Works + +``` +Component calls useScapiFetcher() + ↓ +Hook builds URL: /resource/api/client/{encoded-params} + ↓ +fetcher.load() or fetcher.submit() + ↓ +resource.api.client.$resource.ts loader/action runs ON SERVER + ↓ +createApiClients(context) makes SCAPI call (server-side) + ↓ +JSON response returned to component +``` + +Even though `useScapiFetcher` is called from the browser, the actual SCAPI requests happen **on the server** through the resource route, keeping credentials secure. + +## API + +```typescript +const fetcher = useScapiFetcher( + clientName, // SCAPI client: 'shopperSearch', 'shopperProducts', etc. + methodName, // Method: 'getSearchSuggestions', 'productSearch', etc. + parameters // SCAPI parameters object +); + +// Properties +fetcher.data // Response data (undefined until loaded) +fetcher.state // 'idle' | 'loading' | 'submitting' +fetcher.load() // Trigger a GET request +fetcher.submit() // Trigger a POST request +``` + +## loader vs useScapiFetcher + +| Scenario | Use | +|----------|-----| +| Load product data on page visit | `loader` | +| Load checkout data | `loader` | +| Search suggestions as user types | `useScapiFetcher` | +| Update customer profile in modal | `useScapiFetcher` | +| Load recommendations after page loads | `useScapiFetcher` | +| Fetch bonus products when modal opens | `useScapiFetcher` | +| Infinite scroll / Load more | `useScapiFetcher` | + +## Timeline Comparison + +``` +loader (Server): + [navigate] → [server fetch] → [stream to client] + Data available: Streamed during render via Suspense + +useScapiFetcher: + [render] → [user action] → [fetch] → [re-render] + Data available: AFTER user action, component re-renders +``` + +## Complete Example: Search Suggestions + +```typescript +import { useScapiFetcher } from '@/hooks/use-scapi-fetcher'; +import { useMemo, useCallback } from 'react'; + +export function useSearchSuggestions({ q, limit, currency }) { + const parameters = useMemo( + () => ({ + params: { + query: { q, limit, currency } + } + }), + [q, limit, currency] + ); + + const fetcher = useScapiFetcher( + 'shopperSearch', + 'getSearchSuggestions', + parameters + ); + + const refetch = useCallback(async () => { + await fetcher.load(); + }, [fetcher]); + + return { + data: fetcher.data, + isLoading: fetcher.state === 'loading', + refetch + }; +} +``` diff --git a/skills/storefront-next/skills/sfnext-deployment/SKILL.md b/skills/storefront-next/skills/sfnext-deployment/SKILL.md new file mode 100644 index 00000000..169bf378 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-deployment/SKILL.md @@ -0,0 +1,126 @@ +--- +name: sfnext-deployment +description: Build and deploy Storefront Next storefronts to Managed Runtime (MRT) using the sfnext CLI. Use when running production builds, pushing bundles to MRT with sfnext push, configuring deployment environments, or deploying Page Designer cartridges. This is for Storefront Next deployment — for general MRT management via b2c CLI, see b2c-cli:b2c-mrt. +--- + +# Deployment Skill + +This skill covers building and deploying Storefront Next storefronts to Managed Runtime (MRT). + +## Overview + +Storefront Next storefronts are deployed to MRT as bundles. The `sfnext` CLI handles building and pushing bundles, while environment configuration is managed through MRT environment variables. + +## Production Build + +```bash +# Build for production +pnpm build + +# The build output goes to build/ directory +``` + +The production build: + +- Compiles TypeScript to JavaScript +- Bundles client and server code separately +- Optimizes and minifies assets +- Generates the static Page Designer registry + +## Deploying to MRT + +### Using sfnext CLI + +```bash +# Push the current build to MRT +pnpm push + +# Push with a specific message +pnpm sfnext push -m "Release v1.2.0" + +# Push to a specific environment +pnpm sfnext push --environment staging --wait +``` + +### Deployment Flow + +``` +pnpm build → pnpm push → MRT receives bundle → Deployed to environment +``` + +See [MRT Deployment Reference](references/MRT-DEPLOYMENT.md) for detailed deployment options. + +## Environment Configuration + +Environment variables for MRT are configured through: + +1. **MRT Dashboard** — Set `PUBLIC__` variables per environment (baked into the app at build time) +2. **CLI flags or `MRT_*` environment variables** — Control push/deploy targets +3. **`.env` files** — Local development only (not deployed) + +### MRT Deployment Variables + +```bash +# Project slug (required for push) +MRT_PROJECT=my-project-slug + +# Target environment (optional — if omitted, bundle is uploaded but not deployed) +MRT_TARGET=development +``` + +### Application Variables (set in MRT Dashboard) + +```bash +PUBLIC__app__commerce__api__clientId=prod-client-id +PUBLIC__app__commerce__api__organizationId=prod-org-id +PUBLIC__app__commerce__api__shortCode=prod-short-code +``` + +## Page Designer Cartridge Deployment + +Page Designer metadata must be deployed separately to Commerce Cloud (not MRT): + +```bash +# Generate cartridge metadata +pnpm generate:cartridge + +# Deploy cartridge to B2C instance +pnpm deploy:cartridge + +# Deploy with clean (removes old cartridge first) +pnpm deploy:cartridge:clean + +# Validate cartridge structure +pnpm validate:cartridge +``` + +Cartridge metadata is also auto-generated as part of `pnpm build`. + +## Pre-Deployment Checklist + +1. **Run tests** — `pnpm test` +2. **Check bundle size** — `pnpm bundlesize:test` +3. **Verify environment variables** — All required vars set in target environment +4. **Build successfully** — `pnpm build` completes without errors +5. **Verify SCAPI credentials** — Client ID and org ID match the target environment + +## Troubleshooting + +| Issue | Cause | Solution | +| ------------------------------ | ----------------------------- | ------------------------------------------- | +| Build fails | TypeScript errors | Fix type errors; run `pnpm typecheck` | +| Push rejected | Authentication issue | Verify sfnext CLI credentials | +| 500 errors after deploy | Missing environment variables | Check all required vars in MRT dashboard | +| Stale Page Designer components | Cartridge not deployed | Re-deploy cartridge via MCP tool or b2c CLI | + +## Related Skills + +- `storefront-next:sfnext-project-setup` - Project structure and build configuration +- `storefront-next:sfnext-configuration` - Environment variable configuration +- `storefront-next:sfnext-page-designer` - Page Designer cartridge deployment +- `storefront-next:sfnext-performance` - Bundle size optimization before deployment +- `b2c-cli:b2c-mrt` - General MRT management via b2c CLI (NOT Storefront Next specific) + +## Reference Documentation + +- [MRT Deployment Reference](references/MRT-DEPLOYMENT.md) - Detailed deployment options and configuration diff --git a/skills/storefront-next/skills/sfnext-deployment/references/MRT-DEPLOYMENT.md b/skills/storefront-next/skills/sfnext-deployment/references/MRT-DEPLOYMENT.md new file mode 100644 index 00000000..2d0cebdb --- /dev/null +++ b/skills/storefront-next/skills/sfnext-deployment/references/MRT-DEPLOYMENT.md @@ -0,0 +1,92 @@ +# MRT Deployment Reference + +## Managed Runtime (MRT) + +MRT is the hosting platform for Storefront Next storefronts. It provides: + +- **Server-side rendering** — Node.js runtime for SSR and loader execution +- **CDN** — Global content delivery for static assets +- **Environment management** — Separate environments for development, staging, production +- **Bundle management** — Versioned deployments with rollback capability + +## Deployment Commands + +```bash +# Build and push in one step +pnpm build && pnpm push + +# Push with deployment message +pnpm sfnext push -m "Fix checkout flow" + +# Push to specific environment +pnpm sfnext push --environment production --wait + +# Create a bundle without deploying (inspection/custom pipelines) +pnpm sfnext create-bundle -d . -o .bundle +``` + +## Environment Variables on MRT + +### Setting Variables + +Environment variables are set per-environment through: + +1. **MRT Dashboard** — UI for managing environment variables +2. **CLI/.env values** — `SFCC_MRT_*` values used by `pnpm sfnext push` + +### Variable Limits + +| Constraint | Limit | +| ----------------------- | ------------------ | +| Variable name length | 512 characters max | +| Total `PUBLIC__` values | 32KB max | +| Nesting depth | 10 levels max | + +### Production Configuration Example + +```bash +# Commerce API credentials +PUBLIC__app__commerce__api__clientId=prod-client-id +PUBLIC__app__commerce__api__organizationId=f_ecom_abcd_001 +PUBLIC__app__commerce__api__siteId=RefArchGlobal +PUBLIC__app__commerce__api__shortCode=kv7kzm78 + +# Site configuration +PUBLIC__app__defaultSiteId=RefArchGlobal +PUBLIC__app__commerce__sites='[{"id":"RefArchGlobal","defaultLocale":"en-US","defaultCurrency":"USD","supportedLocales":[{"id":"en-US","preferredCurrency":"USD"},{"id":"de-DE","preferredCurrency":"EUR"}],"supportedCurrencies":["USD","EUR"]}]' + +# Server-only secrets (not exposed to client) +COMMERCE_API_SLAS_SECRET=production-slas-secret +``` + +## Bundle Management + +Each `sfnext push` creates a versioned bundle on MRT: + +``` +Bundle v1 (active) ← current production +Bundle v2 ← previous deployment +Bundle v3 ← two deployments ago +``` + +### Rollback + +If a deployment causes issues, roll back to a previous bundle via the MRT Dashboard or CLI. + +## Multi-Environment Setup + +| Environment | Purpose | Auto-deploy | +| ----------- | ------------------------- | --------------------- | +| Development | Feature testing | From feature branches | +| Staging | Pre-production validation | From main branch | +| Production | Live storefront | Manual promotion | + +## Deployment Verification + +After deploying, verify: + +1. **Health check** — Site loads without errors +2. **SCAPI connectivity** — Products and categories display correctly +3. **Authentication** — Login/logout flow works +4. **Page Designer** — Merchant-editable pages render correctly +5. **Performance** — No regression in page load times diff --git a/skills/storefront-next/skills/sfnext-extensions/SKILL.md b/skills/storefront-next/skills/sfnext-extensions/SKILL.md new file mode 100644 index 00000000..b2cb674a --- /dev/null +++ b/skills/storefront-next/skills/sfnext-extensions/SKILL.md @@ -0,0 +1,116 @@ +--- +name: sfnext-extensions +description: Build extensions for Storefront Next using target-config.json, target points, extension routes, and translation namespaces. Use when creating modular features, inserting components into UI targets, adding extension routes, or using SFDC_EXT_ integration markers. Covers extension structure, targetId configuration, and extension registration in src/extensions/config.json. +--- + +# Extensions Skill + +This skill covers the Storefront Next extension system — modular features that plug into the storefront via target points. + +## Overview + +Extensions are self-contained feature modules that add components, routes, translations, and providers to a Storefront Next storefront without modifying core code. + +## Extension Structure + +``` +src/extensions/my-extension/ +├── target-config.json # Target configuration (components/providers) +├── components/ # Extension components +├── routes/ # Extension routes (auto-registered) +├── locales/ # Extension translations (auto-namespaced) +└── providers/ # Extension context providers +``` + +## Target Configuration + +The `target-config.json` file declares how the extension integrates: + +```json +{ + "components": [ + { + "targetId": "header.before.cart", + "path": "extensions/my-extension/components/badge.tsx", + "order": 0 + } + ], + "contextProviders": [ + { + "path": "extensions/my-extension/providers/my-provider.tsx", + "order": 0 + } + ] +} +``` + +### Target Points + +Target points are named insertion slots in the storefront layout (for example, `header.before.cart`). Extensions insert components at these points via `targetId`. + +Extension availability is managed in `src/extensions/config.json`, where each extension is keyed by an `SFDC_EXT_*` marker. + +## Extension Routes + +Files in the `routes/` directory auto-register as routes: + +```typescript +// src/extensions/my-extension/routes/my-route.tsx +export function loader() { + return { message: 'Hello' }; +} + +export default function MyRoute() { + const { message } = useLoaderData(); + return
{message}
; +} +``` + +## Extension Translations + +Translations are auto-namespaced as `extPascalCase` based on the directory name: + +``` +src/extensions/my-extension/locales/ +├── en-US/translations.json +└── it-IT/translations.json +``` + +```typescript +const {t} = useTranslation('extMyExtension'); +t('welcome'); +``` + +## Integration Markers + +Mark extension integration points in core code: + +```typescript +// Single line marker +/** @sfdc-extension-line SFDC_EXT_MY_FEATURE */ +import myFeature from '@extensions/my-feature'; + +// Block marker +{/* @sfdc-extension-block-start SFDC_EXT_MY_FEATURE */} +My Feature +{/* @sfdc-extension-block-end SFDC_EXT_MY_FEATURE */} +``` + +See [Extension Examples Reference](references/EXTENSION-EXAMPLES.md) for complete examples. + +## Best Practices + +1. **Self-contained** — Each extension should be independent +2. **Use target points** — Insert UI via `target-config.json` rather than editing core files +3. **Namespace translations** — Auto-namespacing prevents collisions +4. **Order matters** — Use `order` in `target-config.json` to control rendering order + +## Related Skills + +- `storefront-next:sfnext-i18n` - Translation patterns and namespace usage +- `storefront-next:sfnext-routing` - How extension routes integrate with the router +- `storefront-next:sfnext-components` - Component patterns used in extensions + +## Reference Documentation + +- [Extension Examples Reference](references/EXTENSION-EXAMPLES.md) - Complete extension examples diff --git a/skills/storefront-next/skills/sfnext-extensions/references/EXTENSION-EXAMPLES.md b/skills/storefront-next/skills/sfnext-extensions/references/EXTENSION-EXAMPLES.md new file mode 100644 index 00000000..7bcaed76 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-extensions/references/EXTENSION-EXAMPLES.md @@ -0,0 +1,116 @@ +# Extension Examples Reference + +## Store Locator Extension + +A complete extension adding store locator functionality: + +``` +src/extensions/store-locator/ +├── target-config.json +├── components/ +│ ├── store-locator-badge.tsx # Badge in header +│ └── store-locator-map.tsx # Map component +├── routes/ +│ └── store-locator.tsx # /store-locator page +├── locales/ +│ ├── en-US/translations.json +│ └── de-DE/translations.json +└── providers/ + └── store-provider.tsx # Store data context +``` + +### target-config.json + +```json +{ + "components": [ + { + "targetId": "header.after.logo", + "path": "extensions/store-locator/components/store-locator-badge.tsx", + "order": 0 + } + ], + "contextProviders": [ + { + "path": "extensions/store-locator/providers/store-provider.tsx", + "order": 0 + } + ] +} +``` + +### Route + +```typescript +// src/extensions/store-locator/routes/store-locator.tsx +import { createApiClients } from '@/lib/api-clients'; +import { useTranslation } from 'react-i18next'; + +export function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + return { + stores: clients.shopperStores.getStores({...}).then(({ data }) => data), + }; +} + +export default function StoreLocatorPage({ loaderData }) { + const { t } = useTranslation('extStoreLocator'); + + return ( +
+

{t('title')}

+ {/* Render store map and list */} +
+ ); +} +``` + +### Translations + +```json +// src/extensions/store-locator/locales/en-US/translations.json +{ + "title": "Find a Store", + "searchPlaceholder": "Enter city or zip code", + "noResults": "No stores found nearby" +} +``` + +## Integration Marker Patterns + +### Single-Line Import + +```typescript +/** @sfdc-extension-line SFDC_EXT_STORE_LOCATOR */ +import StoreLocatorBadge from '@extensions/store-locator/components/store-locator-badge'; +``` + +### Block Integration + +```typescript +/* @sfdc-extension-block-start SFDC_EXT_STORE_LOCATOR */ + + + {t('storeLocator')} + +/* @sfdc-extension-block-end SFDC_EXT_STORE_LOCATOR */ +``` + +## BOPIS Extension (Buy Online, Pick Up In Store) + +```json +{ + "components": [ + { + "targetId": "product.detail.fulfillment", + "path": "extensions/bopis/components/pickup-selector.tsx", + "order": 0 + }, + { + "targetId": "cart.item.fulfillment", + "path": "extensions/bopis/components/pickup-info.tsx", + "order": 0 + } + ] +} +``` diff --git a/skills/storefront-next/skills/sfnext-hybrid-storefronts/SKILL.md b/skills/storefront-next/skills/sfnext-hybrid-storefronts/SKILL.md new file mode 100644 index 00000000..91e94a70 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-hybrid-storefronts/SKILL.md @@ -0,0 +1,148 @@ +--- +name: sfnext-hybrid-storefronts +description: Implement hybrid storefronts that run Storefront Next alongside SFRA or SiteGenesis. Use when planning gradual migration from SFRA, configuring hybrid proxy for local development, setting up session bridging between storefronts, or routing traffic between old and new implementations. +--- + +# Hybrid Storefronts Skill + +This skill covers running Storefront Next alongside an existing SFRA or SiteGenesis storefront — enabling gradual migration without a full rewrite. + +## Overview + +A hybrid storefront splits traffic between Storefront Next (for new or migrated pages) and an existing SFRA/SiteGenesis storefront (for pages not yet migrated). Session bridging ensures users maintain authentication and cart state across both implementations. + +## Architecture + +``` +Customer Request + ↓ + CDN / eCDN (Cloudflare) ← Production: routes based on URL patterns + ↓ + ┌─────────────────────────────┐ + │ Storefront Next │ SFRA/SiteGenesis │ + │ (MRT) │ (B2C Instance) │ + └────────┴────────────────────┘ + ↕ Session Bridge (shared cookies) ↕ +``` + +**Production:** Cloudflare eCDN handles routing between Storefront Next and SFRA based on origin rules. + +**Local Development:** A Vite dev server plugin (`hybridProxyPlugin` from `@salesforce/storefront-next-dev`) proxies non-matching requests to your SFCC sandbox, simulating the eCDN split at `localhost:5173`. + +## Configuration + +Hybrid mode is configured entirely through environment variables — there is no extension directory. + +### Application Config (All Environments) + +These `PUBLIC__` variables are bundled into the app and required in all environments: + +```bash +# Enable hybrid mode — activates the client-side legacy-routes middleware +PUBLIC__app__hybrid__enabled=true + +# Routes that belong to SFRA — client-side clicks to these trigger full-page loads +# Supports exact paths and React Router parameterized routes +PUBLIC__app__hybrid__legacyRoutes='["/cart", "/checkout", "/product/:id"]' +``` + +### Proxy Plugin Config (Local Development Only) + +These configure the Vite dev server proxy and have no effect in production: + +```bash +# Enable the proxy +HYBRID_PROXY_ENABLED=true + +# Your SFCC sandbox origin +SFCC_ORIGIN=https://zzrf-001.dx.commercecloud.salesforce.com + +# Routing rules: Cloudflare eCDN expression format +# Paths matching → Storefront Next; paths not matching → proxied to SFCC +HYBRID_ROUTING_RULES='(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/product.*" or http.request.uri.path matches "^/category.*" or http.request.uri.path matches "^/resource.*" or http.request.uri.path matches "^/action/.*")' + +# Optional: locale for SFRA path transformation (falls back to PUBLIC__app__i18n__fallbackLng) +HYBRID_PROXY_LOCALE=en-GB +``` + +## Client-Side Navigation Middleware + +The `legacy-routes.client.ts` middleware intercepts client-side navigation to SFRA-owned routes: + +1. User clicks `` +2. React Router begins client-side navigation +3. Middleware checks if `/checkout` matches any pattern in `legacyRoutes` +4. If yes → forces a full-page navigation so the CDN/proxy routes to SFRA +5. If no → continues normal client-side rendering + +This prevents React Router from trying to render pages that belong to SFRA. + +## Session Bridging + +Both storefronts share the same session cookies (`dwsid`, `cc-*`) on a common domain: + +### Cookie Handling (Local Dev) + +The proxy handles cookies in three layers: +1. **Set-Cookie header rewriting** — SFCC response `Domain=.salesforce.com` → `Domain=localhost` +2. **Storefront Next server cookies** — Written directly to localhost, no rewriting needed +3. **Client-side cookie interception** — Injected script patches `document.cookie` for SFRA's JS + +### SFRA to Storefront Next + +When navigating from SFRA to Storefront Next: +1. SFRA provides session credentials (dwsgst/dwsrst tokens) +2. Storefront Next exchanges the bridge token for SLAS tokens +3. Normal SLAS cookie-based auth resumes + +### Storefront Next to SFRA + +When navigating from Storefront Next to SFRA: +1. Shared `dwsid` cookie maintains session continuity +2. Full-page navigation triggered by legacy-routes middleware +3. CDN/proxy routes request to SFCC origin + +## Traffic Routing Patterns + +### Gradual Migration Strategy + +1. **Phase 1** — Homepage, product, and category pages on Storefront Next +2. **Phase 2** — Account and search pages +3. **Phase 3** — Cart and checkout flow +4. **Phase 4** — Full migration, remove SFRA + +### Required Routing Rules + +Some patterns must always route to Storefront Next: + +| Pattern | Why | +| -------------- | ---------------------------------------------- | +| `^/resource.*` | React Router resource routes (data endpoints) | +| `^/action/.*` | React Router actions (form submissions) | + +### Keep Rules in Sync + +`HYBRID_ROUTING_RULES` (what Storefront Next owns) and `PUBLIC__app__hybrid__legacyRoutes` (what SFRA owns) are complementary. Any path not in routing rules that could be a `` target should be in `legacyRoutes`. + +## Common Considerations + +- **Cookie domains** — Both storefronts must share a cookie domain for session bridging +- **CDN configuration** — Update eCDN origin rules as pages migrate +- **SEO continuity** — Maintain URL structure to preserve search rankings +- **Cart synchronization** — Basket state is consistent via shared session cookies + +## Troubleshooting + +| Issue | Cause | Solution | +| --------------------------------- | -------------------------- | ------------------------------------------------- | +| Lost session crossing storefronts | Cookie domain mismatch | Ensure shared parent domain | +| Cart items disappear | Basket not synced | Verify session bridge cookies (`dwsid`, `cc-*`) | +| Redirect loops | Conflicting routing rules | Check eCDN rules and `legacyRoutes` consistency | +| 404 on SFRA pages (local dev) | Missing from routing rules | Add path to `HYBRID_ROUTING_RULES` | +| React Router 404 on legacy route | Missing from legacyRoutes | Add path to `PUBLIC__app__hybrid__legacyRoutes` | + +## Related Skills + +- `storefront-next:sfnext-authentication` - SLAS token management and session cookies +- `storefront-next:sfnext-deployment` - MRT deployment for hybrid setup +- `storefront-next:sfnext-configuration` - Environment configuration for hybrid mode diff --git a/skills/storefront-next/skills/sfnext-hybrid-storefronts/references/HYBRID-PROXY-CONFIG.md b/skills/storefront-next/skills/sfnext-hybrid-storefronts/references/HYBRID-PROXY-CONFIG.md new file mode 100644 index 00000000..a28d2d24 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-hybrid-storefronts/references/HYBRID-PROXY-CONFIG.md @@ -0,0 +1,123 @@ +# Hybrid Proxy Configuration Reference + +## Overview + +The hybrid proxy is a **Vite dev server plugin** (`hybridProxyPlugin` from `@salesforce/storefront-next-dev`) that routes requests between Storefront Next and SFRA based on URL patterns during local development. In production, Cloudflare eCDN handles routing. + +## Environment Variables + +### All Environments + +```bash +# Enable hybrid mode (activates client-side legacy-routes middleware) +PUBLIC__app__hybrid__enabled=true + +# Routes owned by SFRA — Link clicks to these force full-page navigation +# Supports exact paths and React Router parameterized routes (/product/:id) +PUBLIC__app__hybrid__legacyRoutes='["/cart", "/checkout"]' +``` + +### Local Development Only + +```bash +# Enable the Vite proxy +HYBRID_PROXY_ENABLED=true + +# SFCC sandbox URL (the actual hostname, not the SCAPI base URL) +SFCC_ORIGIN=https://zzrf-001.dx.commercecloud.salesforce.com + +# Cloudflare-style routing expression +# Paths matching → Storefront Next; paths not matching → proxied to SFCC +HYBRID_ROUTING_RULES='(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/product.*" or http.request.uri.path matches "^/category.*" or http.request.uri.path matches "^/search.*" or http.request.uri.path matches "^/account.*" or http.request.uri.path matches "^/resource.*" or http.request.uri.path matches "^/action/.*")' + +# Optional: locale for SFRA path transformation +HYBRID_PROXY_LOCALE=en-GB + +# Commerce Cloud site ID (likely already set) +PUBLIC__app__defaultSiteId=RefArchGlobal +``` + +## Routing Rules Format + +Each clause follows `http.request.uri.path matches ""`, joined with `or`: + +``` +(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/category.*") +``` + +This is the same format used by Cloudflare eCDN origin rules — keep local and production in sync. + +### Required Patterns + +| Pattern | Why | +| -------------- | ------------------------------------------------ | +| `^/resource.*` | React Router resource routes (server endpoints) | +| `^/action/.*` | React Router actions (form submissions) | + +### Automatically Excluded Paths (never proxied) + +- `/@*`, `/__*` — Vite internals +- `/src/*`, `/node_modules/*` — Source files +- `*.data` — React Router data requests +- `/mobify/*` — SCAPI proxy paths +- Static asset extensions (`.js`, `.css`, `.png`, `.woff2`, etc.) + +SFRA static assets (`/on/demandware.static/*`, `/on/demandware.store/*`) are always proxied. + +## Path Transformation + +The proxy rewrites paths to SFRA format automatically: + +| Browser URL | Proxied to SFCC as | +| -------------- | ------------------------------------- | +| `/cart` | `/s/RefArchGlobal/en-GB/cart` | +| `/checkout` | `/s/RefArchGlobal/en-GB/checkout` | + +The `siteId` comes from `PUBLIC__app__defaultSiteId`. The locale uses `HYBRID_PROXY_LOCALE` → `PUBLIC__app__i18n__fallbackLng` → `default`. + +## Cookie Handling (Local Dev) + +Three layers keep cookies working on localhost: + +1. **Set-Cookie header rewriting** — `Domain=.salesforce.com` → `Domain=localhost` +2. **Storefront Next server cookies** — Written directly to localhost +3. **Client-side cookie interception** — Injected script patches `document.cookie` for SFRA JS + +## Custom Route Matching + +Override the default `shouldRouteToNext` matcher in `vite.config.ts`: + +```typescript +import { hybridProxyPlugin, shouldRouteToNext } from '@salesforce/storefront-next-dev'; + +hybridProxyPlugin({ + routeMatcher: (pathname, rules) => { + if (pathname === '/my-custom-page') return true; // → Storefront Next + if (pathname === '/legacy-only') return false; // → SFRA + return shouldRouteToNext(pathname, rules); // default + }, +}); +``` + +## CDN Routing (Production) + +In production, Cloudflare eCDN origin rules handle the split: + +``` +# Conceptual — configured in Cloudflare dashboard +/ → Storefront Next (MRT origin) +/product/* → Storefront Next (MRT origin) +/category/* → Storefront Next (MRT origin) +/cart → SFRA (B2C Instance origin) +/checkout/* → SFRA (B2C Instance origin) +/on/demandware.static/* → SFRA (B2C Instance origin) +``` + +## Shared Cookie Requirements + +Both storefronts must share a parent cookie domain: + +| Storefront | Domain | Cookie Domain | +| --------------- | ------------------- | --------------- | +| Storefront Next | `www.example.com` | `.example.com` | +| SFRA | `legacy.example.com`| `.example.com` | diff --git a/skills/storefront-next/skills/sfnext-i18n/SKILL.md b/skills/storefront-next/skills/sfnext-i18n/SKILL.md new file mode 100644 index 00000000..0068e487 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-i18n/SKILL.md @@ -0,0 +1,187 @@ +--- +name: sfnext-i18n +description: Implement internationalization in Storefront Next using i18next with useTranslation for components and getTranslation for server-side code. Use when adding translations, configuring locales, handling pluralization, using the Zod schema factory pattern, or managing extension translations. Covers namespaces, interpolation, and language switching. +--- + +# Internationalization (i18n) Skill + +This skill covers internationalization in Storefront Next using i18next with a dual-instance architecture (server + client). + +## Overview + +- **Server instance** — Has access to all translations for all languages +- **Client instance** — Dynamically imports translations as JavaScript chunks +- **Dual API** — `useTranslation()` for components, `getTranslation()` for non-component code + +## Translation File Structure + +Translations are organized as namespace files per locale, compiled into a TypeScript index: + +``` +src/locales/en-US/ +├── index.ts # Merges all namespace files + extensions +├── translations.json # Default namespace (top-level keys become namespaces) +└── product.json # "product" namespace (separate file) + +src/locales/en-GB/ +├── index.ts +├── translations.json +└── product.json +``` + +The `index.ts` imports all namespace files and extension translations: + +```typescript +import translations from '@/locales/en-US/translations.json'; +import product from '@/locales/en-US/product.json'; +import extensionTranslations from '@/extensions/locales/en-US/'; + +const allTranslations = { ...translations, product, ...extensionTranslations }; +export default allTranslations satisfies ResourceLanguage; +``` + +### Namespace structure in `translations.json` + +Each top-level key is a namespace: + +```json +{ + "header": { + "search": "Search", + "account": "Account" + }, + "footer": { + "copyright": "© {{year}} Company" + } +} +``` + +### Separate namespace file (`product.json`) + +```json +{ + "title": "Product Details", + "addToCart": "Add to Cart", + "greeting": "Hello, {{name}}!", + "itemCount_one": "{{count}} item", + "itemCount_other": "{{count}} items" +} +``` + +## Usage in Components + +```typescript +import { useTranslation } from 'react-i18next'; + +export function ProductCard() { + const { t } = useTranslation('product'); + + return ( +
+

{t('title')}

+ +

{t('greeting', { name: 'John' })}

+

{t('itemCount', { count: 5 })}

+
+ ); +} +``` + +**Critical:** Always pass a namespace to `useTranslation()`: + +```typescript +// WRONG — missing namespace +const { t } = useTranslation(); +t('title'); // Will not find the key + +// CORRECT — with namespace +const { t } = useTranslation('product'); +t('title'); // Works +``` + +## Usage in Server Code + +```typescript +import { getTranslation } from '@/lib/i18next'; + +// In loaders/actions (pass context) +export function loader(args: LoaderFunctionArgs) { + const { t } = getTranslation(args.context); + return { title: t('product:title') }; +} + +// Client-side utilities (no context) +const { t } = getTranslation(); +const message = t('product:addToCart'); +``` + +## Validation Schemas — Factory Pattern + +**Critical:** Use a factory function for Zod schemas with translated messages to avoid race conditions: + +```typescript +// WRONG — Module-level schema (race condition: t() may not be initialized) +export const schema = z.object({ + email: z.string().email(t('validation:emailInvalid')) +}); + +// CORRECT — Factory function +import type { TFunction } from 'i18next'; + +export const createSchema = (t: TFunction) => { + return z.object({ + email: z.string().email(t('validation:emailInvalid')) + }); +}; + +// Usage in component +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function MyForm() { + const { t } = useTranslation(); + const schema = useMemo(() => createSchema(t), [t]); + + const form = useForm({ resolver: zodResolver(schema) }); +} +``` + +## Language Switching + +```typescript +import LocaleSwitcher from '@/components/locale-switcher'; + +export function Footer() { + return
; +} +``` + +## Extension Translations + +Extensions use `extPascalCase` namespace auto-derived from the extension directory name: + +``` +src/extensions/my-extension/locales/ +├── en-US/translations.json +└── it-IT/translations.json +``` + +```typescript +const { t } = useTranslation('extMyExtension'); +t('welcome'); +``` + +## Common Pitfalls + +| Pitfall | Problem | Solution | +|---------|---------|----------| +| Missing namespace | Keys not found | Always pass namespace: `useTranslation('product')` | +| Module-level `t()` in schemas | Race condition on initialization | Use factory pattern: `createSchema(t)` | +| Forgetting context on server | Translations not found | Use `getTranslation(args.context)` in loaders | +| Duplicate keys across namespaces | Wrong translation shown | Prefix with namespace: `t('product:title')` | + +## Related Skills + +- `storefront-next:sfnext-components` - Using translations in UI components +- `storefront-next:sfnext-extensions` - Extension translation namespacing +- `storefront-next:sfnext-configuration` - Locale and site configuration diff --git a/skills/storefront-next/skills/sfnext-page-designer/SKILL.md b/skills/storefront-next/skills/sfnext-page-designer/SKILL.md new file mode 100644 index 00000000..5f375123 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-page-designer/SKILL.md @@ -0,0 +1,181 @@ +--- +name: sfnext-page-designer +description: Integrate Page Designer with Storefront Next using React decorators, component registry, and Region rendering. Use when creating merchant-editable pages, adding Page Designer components with @Component/@AttributeDefinition decorators, using fetchPageFromLoader, or rendering Regions. This is for the React/Storefront Next implementation — for classic ISML Page Designer, see b2c:b2c-page-designer. +--- + +# Page Designer Skill + +This skill covers Page Designer integration in Storefront Next — building merchant-editable pages with React components, decorators, and the Shopper Experience API. + +## Overview + +Page Designer is Commerce Cloud's visual editor for merchants. In Storefront Next, page structure (regions, components, attributes) is fetched from the Shopper Experience API and rendered via a component registry and `` component. + +| Concept | Role | +|---------|------| +| **Page** | Fetched in route loaders via `fetchPageFromLoader(args, { pageId })` | +| **Region** | Named area rendered with `` | +| **Component** | Content block with a `typeId` and attributes; registered in `@/lib/registry` | +| **Registry** | Static registry auto-generated by Vite plugin — do not edit by hand | + +## Which Pages Use Page Designer + +| Uses Page Designer | Does Not | +|--------------------|----------| +| Home (`homepage`), Category/PLP, Search, Product/PDP | Cart, Checkout, Account, Auth | + +## Route Setup + +### In the Loader + +```typescript +import { fetchPageFromLoader, collectComponentDataPromises } from '@/lib/page-designer'; + +export function loader(args: LoaderFunctionArgs) { + const pagePromise = fetchPageFromLoader(args, { pageId: 'homepage' }); + const componentData = collectComponentDataPromises(args, pagePromise); + + return { + page: pagePromise, + componentData, + }; +} +``` + +### In the Layout + +```typescript +import { Region } from '@/components/region'; + +export default function HomePage({ loaderData: { page, componentData } }) { + return ( +
+ } + errorElement={} /> + + } /> +
+ ); +} +``` + +## Component Setup + +### Metadata Class with Decorators + +```typescript +import { Component, AttributeDefinition, RegionDefinition } from '@salesforce/storefront-next-runtime/design'; + +@Component('hero-banner', { + name: 'Hero Banner', + description: 'Full-width banner with image and CTA' +}) +class HeroBannerMeta { + @AttributeDefinition({ type: 'image' }) + image: string; + + @AttributeDefinition() + headline: string; + + @AttributeDefinition({ type: 'url' }) + ctaUrl: string; + + @AttributeDefinition() + ctaText: string; + + @AttributeDefinition({ type: 'boolean' }) + fullWidth: boolean; +} +``` + +### React Component + +```typescript +export default function HeroBanner({ image, headline, ctaUrl, ctaText, fullWidth }) { + return ( +
+ +

{headline}

+ {ctaUrl && {ctaText}} +
+ ); +} +``` + +See [Decorator Patterns Reference](references/DECORATOR-PATTERNS.md) for all decorator options. + +### Components with Server Data + +If a component needs server data (e.g., product carousel): + +```typescript +// Export a loader for the component +export function loader({ componentData, context }) { + const clients = createApiClients(context); + return clients.shopperProducts.getProducts({ + params: { query: { ids: componentData.productIds } } + }).then(({ data }) => data); +} + +// Optional fallback during loading +export function fallback() { + return ; +} + +// Component receives data as prop +export default function ProductCarousel({ data, ...props }) { + return ; +} +``` + +See [Component Registry Reference](references/COMPONENT-REGISTRY.md) for registry details. + +## Design Mode + +Detect when running inside Page Designer: + +```typescript +import { isDesignModeActive, isPreviewModeActive } from '@salesforce/storefront-next-runtime/design/mode'; + +// In loaders +const isDesignMode = isDesignModeActive(request); +const isPreview = isPreviewModeActive(request); + +// Root layout exposes pageDesignerMode in loader data +// 'EDIT' | 'PREVIEW' | undefined +``` + +## MCP Tools + +Use the B2C DX MCP server tools for Page Designer development: + +1. **`storefront_next_page_designer_decorator`** — Adds decorators to components (auto mode or interactive) +2. **`storefront_next_generate_page_designer_metadata`** — Generates metadata JSON for Business Manager +3. **`cartridge_deploy`** — Deploys cartridge to Commerce Cloud + +### Typical Workflow + +1. Add decorators to component (use `storefront_next_page_designer_decorator` with autoMode) +2. Generate metadata JSON (`storefront_next_generate_page_designer_metadata`) +3. Deploy cartridge (`cartridge_deploy`) + +## Best Practices + +1. **Keep loaders synchronous** — Return promises for streaming +2. **Use registry** — Register components with proper `typeId` +3. **Handle design mode** — Adapt UI when `pageDesignerMode` is `'EDIT'` or `'PREVIEW'` +4. **Rebuild after changes** — Static registry regenerates at build time +5. **Use MCP tools** — Faster than hand-writing decorators and metadata + +## Related Skills + +- `storefront-next:sfnext-data-fetching` - Loader patterns for Page Designer routes +- `storefront-next:sfnext-components` - Component development patterns +- `b2c:b2c-page-designer` - Classic ISML Page Designer (NOT Storefront Next) + +## Reference Documentation + +- [Decorator Patterns Reference](references/DECORATOR-PATTERNS.md) - All decorator types and options +- [Component Registry Reference](references/COMPONENT-REGISTRY.md) - Registry setup and component loaders diff --git a/skills/storefront-next/skills/sfnext-page-designer/references/COMPONENT-REGISTRY.md b/skills/storefront-next/skills/sfnext-page-designer/references/COMPONENT-REGISTRY.md new file mode 100644 index 00000000..5bb43b3f --- /dev/null +++ b/skills/storefront-next/skills/sfnext-page-designer/references/COMPONENT-REGISTRY.md @@ -0,0 +1,78 @@ +# Component Registry Reference + +## Overview + +The component registry maps Page Designer `typeId` values to React components. The registry is **auto-generated** at build time by the staticRegistry Vite plugin. + +## Static Registry + +The file `src/lib/static-registry.ts` is generated automatically. **Do not edit it by hand.** The Vite plugin scans for components with `@Component` decorators and builds the registry. + +After adding or modifying decorators, rebuild the app to regenerate the registry: + +```bash +pnpm build +# or during development +pnpm dev # Hot reload regenerates automatically +``` + +## Component Registration + +Components are registered by their `@Component` decorator `typeId`: + +```typescript +// The registry maps typeId -> component module +// Example auto-generated entry: +{ + 'hero-banner': () => import('@/components/hero-banner'), + 'product-carousel': () => import('@/components/product-carousel'), + 'content-card': () => import('@/components/content-card'), +} +``` + +## Component Loaders + +Components that need server data export a `loader` function. The registry calls these loaders during `collectComponentDataPromises()` in the route loader: + +```typescript +// In the component file: +export function loader({ componentData, context }) { + const clients = createApiClients(context); + const productIds = componentData.productIds; + + return clients.shopperProducts.getProducts({ + params: { query: { ids: productIds.join(',') } } + }).then(({ data }) => data); +} + +export function fallback() { + return ; +} + +export default function ProductCarousel({ data, ...props }) { + // data = resolved result from loader + return
{data.products.map(p => )}
; +} +``` + +### Data Flow + +``` +Route loader + |-- fetchPageFromLoader(args, { pageId }) -> page promise + +-- collectComponentDataPromises(args, page) -> componentData map + | + For each component with a loader: + component.loader({ componentData, context }) -> data promise + | + renders components with resolved data +``` + +## Adding a New Page Designer Component + +1. Create the React component with decorator metadata class +2. Implement the component's render function +3. (Optional) Export `loader` and `fallback` if server data is needed +4. Rebuild to regenerate static registry +5. Generate metadata JSON via MCP tool (`storefront_next_generate_page_designer_metadata`) +6. Deploy cartridge via MCP tool (`cartridge_deploy`) diff --git a/skills/storefront-next/skills/sfnext-page-designer/references/DECORATOR-PATTERNS.md b/skills/storefront-next/skills/sfnext-page-designer/references/DECORATOR-PATTERNS.md new file mode 100644 index 00000000..3d22602b --- /dev/null +++ b/skills/storefront-next/skills/sfnext-page-designer/references/DECORATOR-PATTERNS.md @@ -0,0 +1,180 @@ +# Decorator Patterns Reference + +## @Component Decorator + +Marks a class as a Page Designer component: + +```typescript +@Component('typeId', { + name: 'Display Name', + description: 'Component description for merchants' +}) +class MyComponentMeta { + // attribute definitions... +} +``` + +- `typeId` — Unique identifier (lowercase, hyphens); used in registry and Business Manager +- `name` — Display name in Page Designer sidebar +- `description` — Merchant-facing description + +## @AttributeDefinition Decorator + +Defines a merchant-editable attribute: + +```typescript +@AttributeDefinition() // Default: string type +headline: string; + +@AttributeDefinition({ type: 'image' }) +heroImage: string; + +@AttributeDefinition({ type: 'url' }) +ctaLink: string; + +@AttributeDefinition({ type: 'boolean' }) +showBadge: boolean; + +@AttributeDefinition({ type: 'enum', values: ['left', 'center', 'right'] }) +alignment: string; + +@AttributeDefinition({ type: 'product' }) +featuredProduct: string; + +@AttributeDefinition({ type: 'category' }) +category: string; + +@AttributeDefinition({ type: 'markup' }) +richText: string; + +@AttributeDefinition({ type: 'integer' }) +maxItems: number; +``` + +### Attribute Types + +| Type | Input in Business Manager | Returns | +|------|--------------------------|---------| +| `string` | Text input | String | +| `text` | Multi-line text | String | +| `markup` | Rich text editor | Markup string | +| `boolean` | Checkbox | Boolean | +| `integer` | Number input | Integer | +| `enum` | Dropdown select | String | +| `image` | Image picker | Image URL | +| `file` | File picker | File URL | +| `url` | URL input | URL string | +| `category` | Category selector | Category ID | +| `product` | Product selector | Product ID | + +### Type Inference by MCP Tool + +The `storefront_next_page_designer_decorator` MCP tool infers types from prop names: + +| Prop Name Pattern | Inferred Type | +|------------------|---------------| +| `*Url`, `*Link`, `*Href` | `url` | +| `*Image`, `*Img`, `*Src` | `image` | +| `is*`, `show*`, `has*`, `enable*` | `boolean` | +| `*Count`, `*Num`, `max*`, `min*` | `integer` | +| Other | `string` | + +## @RegionDefinition Decorator + +Defines nested regions within a component (e.g., a grid with slots): + +```typescript +@Component('content-grid', { name: 'Content Grid', description: '2-column grid' }) +@RegionDefinition([ + { + id: 'left', + name: 'Left Column', + description: 'Left content area', + maxComponents: 3, + }, + { + id: 'right', + name: 'Right Column', + description: 'Right content area', + maxComponents: 3, + }, +]) +class ContentGridMeta { + @AttributeDefinition({ type: 'boolean' }) + equalWidth: boolean; +} +``` + +## @PageType Decorator + +Used on route modules to define page types in Business Manager: + +```typescript +@PageType({ + name: 'Home Page', + description: 'Main landing page', + supportedAspectTypes: ['default'] +}) +@RegionDefinition([ + { id: 'hero', name: 'Hero Section', maxComponents: 1 }, + { id: 'content', name: 'Main Content' }, +]) +``` + +## Complete Component Example + +```typescript +import { Component, AttributeDefinition, RegionDefinition } from '@salesforce/storefront-next-runtime/design'; + +@Component('promo-banner', { + name: 'Promotional Banner', + description: 'Banner with background image, text overlay, and CTA' +}) +class PromoBannerMeta { + @AttributeDefinition({ type: 'image' }) + backgroundImage: string; + + @AttributeDefinition() + headline: string; + + @AttributeDefinition({ type: 'text' }) + subheadline: string; + + @AttributeDefinition({ type: 'url' }) + ctaUrl: string; + + @AttributeDefinition() + ctaLabel: string; + + @AttributeDefinition({ type: 'enum', values: ['light', 'dark'] }) + theme: string; + + @AttributeDefinition({ type: 'boolean' }) + fullWidth: boolean; +} + +export default function PromoBanner({ + backgroundImage, + headline, + subheadline, + ctaUrl, + ctaLabel, + theme = 'light', + fullWidth, +}: PromoBannerProps) { + return ( +
+ +
+

{headline}

+ {subheadline &&

{subheadline}

} + {ctaUrl && {ctaLabel}} +
+
+ ); +} +``` diff --git a/skills/storefront-next/skills/sfnext-performance/SKILL.md b/skills/storefront-next/skills/sfnext-performance/SKILL.md new file mode 100644 index 00000000..55627246 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-performance/SKILL.md @@ -0,0 +1,132 @@ +--- +name: sfnext-performance +description: Optimize Storefront Next performance with bundle size limits, DynamicImage component, Lighthouse audits, and progressive streaming. Use when checking bundle sizes, optimizing images, improving page load speed, or configuring performance metrics. Covers parallel data fetching, image optimization, and performance monitoring. +--- + +# Performance Skill + +This skill covers performance optimization techniques for Storefront Next storefronts. + +## Bundle Size Limits + +The application enforces strict bundle size limits in `package.json` under the `bundlesize` configuration. + +```bash +pnpm bundlesize:test # Verify bundle stays within limits +pnpm bundlesize:analyze # Analyze bundle composition +``` + +## Built-in Metrics + +Enable performance tracking in `config.server.ts`: + +```typescript +{ + performance: { + metrics: { + serverPerformanceMetricsEnabled: true, + clientPerformanceMetricsEnabled: true, + serverTimingHeaderEnabled: false // Enable for debugging only + } + } +} +``` + +Tracks: +- SSR operations and rendering time +- SCAPI API calls with parallelization visibility +- Authentication operations +- Client-side navigation timing + +## Parallel Data Fetching + +Return all promises simultaneously in loaders — avoid sequential `await`: + +```typescript +// GOOD — Parallel (all requests start at once) +export function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + return { + product: clients.shopperProducts.getProduct({...}), + reviews: clients.shopperProducts.getReviews({...}), + recommendations: clients.shopperProducts.getRecommendations({...}), + }; +} + +// BAD — Sequential (each waits for previous) +export async function loader({ context }: LoaderFunctionArgs) { + const product = await clients.shopperProducts.getProduct({...}); + const reviews = await clients.shopperProducts.getReviews({...}); + return { product, reviews }; +} +``` + +## Image Optimization + +Use the `DynamicImage` component with WebP format: + +```typescript +import { DynamicImage } from '@/components/dynamic-image'; + + +``` + +### Image Best Practices + +- Use WebP format by default (smaller file sizes) +- Set explicit `width` and `height` to prevent layout shifts +- Lazy load below-the-fold images +- Use SCAPI image alt text as the primary alt source + +## Progressive Streaming + +Use synchronous loaders returning promises to stream data progressively: + +```typescript +// Streams data as each promise resolves +export function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + return { + product: clients.shopperProducts.getProduct({...}), // Streams independently + reviews: clients.shopperProducts.getReviews({...}), // Streams independently + }; +} +``` + +Combine with granular Suspense boundaries for progressive page rendering. + +## Lighthouse Optimization + +Monitor and improve performance metrics: + +```bash +pnpm lighthouse:ci # Run Lighthouse CI +``` + +**Key areas:** +- Preload critical CSS +- Use WebP images by default +- Lazy load below-the-fold content +- Optimize font loading +- Minimize JavaScript bundle size + +## Troubleshooting + +| Issue | Cause | Solution | +|-------|-------|----------| +| Large bundle size | Unused imports or heavy dependencies | Run `bundlesize:analyze`; tree-shake or lazy load | +| Slow page transitions | Async loaders blocking | Use synchronous loaders returning promises | +| Layout shifts | Missing image dimensions | Set `width` and `height` on images | +| Slow SCAPI responses | Sequential API calls | Use parallel data fetching | + +## Related Skills + +- `storefront-next:sfnext-data-fetching` - Parallel loader patterns for performance +- `storefront-next:sfnext-components` - Suspense boundaries for progressive rendering +- `storefront-next:sfnext-deployment` - Production build optimization diff --git a/skills/storefront-next/skills/sfnext-project-setup/SKILL.md b/skills/storefront-next/skills/sfnext-project-setup/SKILL.md new file mode 100644 index 00000000..904302b3 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-project-setup/SKILL.md @@ -0,0 +1,147 @@ +--- +name: sfnext-project-setup +description: Create and configure Storefront Next projects. Use when creating a new storefront, understanding project structure, setting up environment variables, or running the sfnext CLI for the first time. Covers project creation, directory layout, .env configuration, and sfnext CLI basics. +--- + +# Project Setup Skill + +This skill guides you through creating and configuring a Storefront Next project — a server-rendered SPA built on React 19, React Router 7, Vite, and Tailwind CSS v4. + +## Overview + +Storefront Next storefronts run on Managed Runtime (MRT) with all SCAPI requests executing server-side. Projects are created with `create-storefront` from `@salesforce/storefront-next-dev` and use TypeScript exclusively (`.ts`/`.tsx` files only — `.js`/`.jsx`/`.mjs`/`.cjs` are forbidden). + +## Creating a Project + +### Via CLI (local development) + +```bash +# Create a new storefront project (interactive) +pnpm dlx @salesforce/storefront-next-dev create-storefront + +# Or create with a name flag +pnpm dlx @salesforce/storefront-next-dev create-storefront --name my-storefront + +# Navigate into the project +cd my-storefront + +# Install dependencies +pnpm install + +# Start development server +pnpm dev +``` + +### Via Business Manager + +Projects can also be created from Business Manager, which sets up the storefront with Commerce Cloud credentials pre-configured. + +## Project Structure + +``` +my-storefront/ +├── .env.default # Default environment variables (template) +├── .env # Local overrides (git-ignored) +├── config.server.ts # Centralized configuration with defaults +├── vite.config.ts # Vite build configuration +├── package.json # Dependencies and scripts +├── src/ +│ ├── app.css # Global styles and Tailwind theme +│ ├── root.tsx # Root layout component +│ ├── routes/ # File-based routes (React Router 7) +│ │ ├── _app.tsx # App layout (authenticated shell) +│ │ ├── _app._index.tsx # Home page +│ │ └── _app.product.$productId.tsx +│ ├── components/ # Reusable UI components +│ │ ├── ui/ # shadcn/ui base components (customizable) +│ │ └── ... # Custom components +│ ├── config/ # Configuration schema and context +│ │ └── schema.ts # Config type definitions +│ ├── lib/ # Utilities and API client setup +│ │ ├── api-clients.ts # createApiClients() factory +│ │ ├── utils.ts # cn() utility and helpers +│ │ └── registry.ts # Page Designer component registry +│ ├── locales/ # Translation files +│ │ └── en-US/ +│ │ └── translations.json +│ ├── middlewares/ # Server middleware +│ │ ├── auth.server.ts # Authentication middleware +│ │ └── basket.server.ts # Basket middleware +│ ├── providers/ # React context providers +│ ├── hooks/ # Custom React hooks +│ ├── extensions/ # Extension modules +│ └── test-utils/ # Shared test utilities +├── public/ # Static assets +└── cartridges/ # Page Designer cartridge metadata +``` + +See [Project Structure Reference](references/PROJECT-STRUCTURE.md) for detailed directory explanations. + +## Environment Setup + +Copy `.env.default` to `.env` and configure required Commerce Cloud credentials: + +```bash +cp .env.default .env +``` + +### Required Variables + +```bash +PUBLIC__app__commerce__api__clientId=your-client-id +PUBLIC__app__commerce__api__organizationId=your-org-id +PUBLIC__app__commerce__api__siteId=your-site-id +PUBLIC__app__commerce__api__shortCode=your-short-code +PUBLIC__app__defaultSiteId=your-site-id +PUBLIC__app__commerce__sites='[{"id":"your-site-id","defaultLocale":"en-US","defaultCurrency":"USD","supportedLocales":[{"id":"en-US","preferredCurrency":"USD"}],"supportedCurrencies":["USD"]}]' +``` + +See [Environment Variables Reference](references/ENV-VARIABLES.md) for the complete variable list and naming rules. + +## Development Commands + +```bash +# Start development server with hot reload +pnpm dev + +# Build for production +pnpm build + +# Run tests +pnpm test + +# Run Storybook +pnpm storybook + +# Check bundle size +pnpm bundlesize:test +``` + +## Non-Negotiable Rules + +1. **TypeScript only** — `.ts` and `.tsx` files; JavaScript files are blocked by ESLint +2. **Server-only data loading** — Use `loader` functions; never use client loaders +3. **Synchronous loaders** — Return promises, do not use `async`/`await` (enables streaming) +4. **Tailwind CSS 4 only** — No inline styles, CSS modules, or separate CSS files +5. **Use `createPage()` HOC** — Standardizes page patterns with Suspense + +## Troubleshooting + +| Issue | Cause | Solution | +| ----------------------- | ----------------------------------- | ----------------------------------------------------- | +| SCAPI 401 errors | Missing or invalid credentials | Verify `.env` variables match your Commerce Cloud org | +| JavaScript file errors | `.js`/`.jsx` files in source | Rename to `.ts`/`.tsx`; ESLint blocks JS files | +| Dev server not starting | Missing dependencies | Run `pnpm install` | +| Config not loading | Missing `config.server.ts` defaults | Define defaults before using environment overrides | + +## Related Skills + +- `storefront-next:sfnext-configuration` - Ongoing configuration management and multi-site setup +- `storefront-next:sfnext-routing` - File-based routing conventions +- `storefront-next:sfnext-data-fetching` - Server-side data loading patterns +- `storefront-next:sfnext-deployment` - Building and deploying to MRT + +## Reference Documentation + +- [Project Structure Reference](references/PROJECT-STRUCTURE.md) - Detailed directory and file explanations +- [Environment Variables Reference](references/ENV-VARIABLES.md) - Complete variable list and naming rules diff --git a/skills/storefront-next/skills/sfnext-project-setup/references/ENV-VARIABLES.md b/skills/storefront-next/skills/sfnext-project-setup/references/ENV-VARIABLES.md new file mode 100644 index 00000000..5df44efd --- /dev/null +++ b/skills/storefront-next/skills/sfnext-project-setup/references/ENV-VARIABLES.md @@ -0,0 +1,82 @@ +# Environment Variables Reference + +## Naming Convention + +Environment variables use the `PUBLIC__` prefix with double underscore (`__`) separators to map to configuration paths: + +```bash +# Pattern: PUBLIC__app__{path}__{to}__{property}=value +PUBLIC__app__commerce__api__clientId=abc123 +# Maps to: config.app.commerce.api.clientId +# Accessed as: getConfig(context).commerce.api.clientId +``` + +## Rules + +1. **`PUBLIC__` prefix** — Exposed to browser (client-safe values only) +2. **No prefix** — Server-only secrets (never sent to client) +3. **`__` separator** — Navigates nested config paths +4. **Case-insensitive** — All casings work (normalized to match `config.server.ts`) +5. **Auto-parsing** — Strings, numbers, booleans, JSON arrays/objects parsed automatically +6. **Validation** — Paths must exist in `config.server.ts` (prevents typos) +7. **Depth limit** — Maximum 10 levels deep (use JSON for deeper nesting) +8. **Path precedence** — More specific paths override less specific ones +9. **Protected paths** — `app__engagement` cannot be overridden via environment variables +10. **MRT limits** — Variable names max 512 characters, total `PUBLIC__` values max 32KB + +## Required Variables + +| Variable | Description | +|----------|-------------| +| `PUBLIC__app__commerce__api__clientId` | SLAS client ID | +| `PUBLIC__app__commerce__api__organizationId` | Commerce Cloud organization ID | +| `PUBLIC__app__commerce__api__siteId` | Default site ID | +| `PUBLIC__app__commerce__api__shortCode` | API short code | +| `PUBLIC__app__defaultSiteId` | Default site ID for routing | +| `PUBLIC__app__commerce__sites` | JSON array of site configurations | + +## Server-Only Variables + +```bash +# Server-only secrets (no PUBLIC__ prefix) +COMMERCE_API_SLAS_SECRET=your-slas-secret +``` + +Access server-only secrets via `process.env` directly — never add them to the config system. + +## Multi-Site Configuration + +The `commerce.sites` array defines site configurations: + +```bash +PUBLIC__app__commerce__sites='[ + { + "id": "RefArchGlobal", + "defaultLocale": "en-US", + "defaultCurrency": "USD", + "supportedLocales": [ + {"id": "en-US", "preferredCurrency": "USD"}, + {"id": "de-DE", "preferredCurrency": "EUR"} + ], + "supportedCurrencies": ["USD", "EUR"] + } +]' +``` + +## Setting Complex Values + +```bash +# Individual variables +PUBLIC__app__myFeature__option1=value1 +PUBLIC__app__myFeature__option2=value2 + +# Or as a single JSON value +PUBLIC__app__myFeature='{"option1":"value1","option2":"value2"}' +``` + +## Security Guidelines + +| Prefix | Visibility | Use For | +|--------|-----------|---------| +| `PUBLIC__` | Browser + Server | Client IDs, site IDs, feature flags | +| (none) | Server only | API secrets, private keys, credentials | diff --git a/skills/storefront-next/skills/sfnext-project-setup/references/PROJECT-STRUCTURE.md b/skills/storefront-next/skills/sfnext-project-setup/references/PROJECT-STRUCTURE.md new file mode 100644 index 00000000..c3344a32 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-project-setup/references/PROJECT-STRUCTURE.md @@ -0,0 +1,99 @@ +# Project Structure Reference + +## Top-Level Files + +| File | Purpose | +| ------------------ | ------------------------------------------------------------------------------------------ | +| `config.server.ts` | Centralized configuration with typed defaults; environment variables override these values | +| `.env.default` | Template of all supported environment variables; copy to `.env` for local development | +| `.env` | Local environment overrides (git-ignored) | +| `vite.config.ts` | Vite build configuration including plugins (static registry, etc.) | +| `package.json` | Dependencies, scripts, and bundle size limits (`bundlesize` config) | +| `tsconfig.json` | TypeScript configuration with `@/` path alias mapping to `src/` | + +## Source Directory (`src/`) + +### `src/routes/` + +File-based routing using React Router 7 flat-routes convention. File names map to URL paths: + +| File | URL | +| ----------------------------------- | ------------------------------------ | +| `_app.tsx` | Layout wrapper (no URL segment) | +| `_app._index.tsx` | `/` (home page) | +| `_app.product.$productId.tsx` | `/product/:productId` | +| `_app.category.$categoryId.tsx` | `/category/:categoryId` | +| `_app.cart.tsx` | `/cart` | +| `resource.api.client.$resource.tsx` | Resource route for `useScapiFetcher` | + +Each route module can export: `loader`, `action`, `default` (component), `meta`, `handle`, `ErrorBoundary`. + +### `src/components/` + +Reusable UI components organized in directories: + +``` +src/components/product-tile/ +├── index.tsx # Component implementation +├── index.test.tsx # Vitest unit tests +└── stories/ + └── index.stories.tsx # Storybook stories +``` + +**`src/components/ui/`** — shadcn/ui components added via `npx shadcn@latest add `. These are copied into your codebase and can be customized directly. + +### `src/config/` + +Configuration system: + +- `schema.ts` — TypeScript type definitions for the config shape +- `context.tsx` — React context provider (`ConfigProvider`) and `useConfig()` hook +- `index.ts` — Exports `getConfig()` and `useConfig()` + +### `src/lib/` + +Utilities and shared logic: + +- `api-clients.ts` — `createApiClients(context)` factory for SCAPI clients +- `utils.ts` — `cn()` class name utility (merges Tailwind classes) +- `registry.ts` — Page Designer component registry +- `static-registry.ts` — Auto-generated by Vite plugin (do not edit) + +### `src/locales/` + +Translation files organized by locale: + +``` +src/locales/ +├── en-US/translations.json +├── de-DE/translations.json +└── fr-FR/translations.json +``` + +### `src/middlewares/` + +Server middleware: + +- `auth.server.ts` — SLAS authentication, token management, `getAuth(context)` +- `basket.server.ts` — Basket middleware and request-context basket resource helpers + +### `src/extensions/` + +Extension modules with their own routes, components, translations, and providers. Extensions typically include `target-config.json` (for `targetId` component/provider insertion) and are registered in `src/extensions/config.json`. + +### `src/test-utils/` + +Shared test utilities: + +- `config.ts` — Mock configuration for tests +- `context-provider-utils.ts` — Context provider helpers +- `context-provider.tsx` — Test wrapper providers + +## Build Output + +- `build/` — Production build output (server and client bundles) +- `coverage/` — Test coverage reports + +## Cartridges + +- `cartridges/` — Page Designer cartridge with experience metadata (component/page JSON definitions generated by MCP tools) diff --git a/skills/storefront-next/skills/sfnext-routing/SKILL.md b/skills/storefront-next/skills/sfnext-routing/SKILL.md new file mode 100644 index 00000000..deca0ee0 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-routing/SKILL.md @@ -0,0 +1,180 @@ +--- +name: sfnext-routing +description: Implement file-based routing in Storefront Next with React Router 7. Use when adding new pages, creating layout routes, defining route parameters, or understanding route module exports (loader, action, component, meta). Covers flat-routes conventions, nested layouts, and the _app prefix. +--- + +# Routing Skill + +This skill covers React Router 7 file-based routing in Storefront Next projects. + +## Overview + +Storefront Next uses React Router 7 in framework mode with flat-routes file conventions. Route files live in `src/routes/` and file names map directly to URL paths using dot (`.`) separators for nesting. + +## Route File Conventions + +### File Name to URL Mapping + +| File Name | URL Path | Notes | +|-----------|----------|-------| +| `_app.tsx` | — | Layout route (wraps child routes) | +| `_app._index.tsx` | `/` | Home page (index of `_app` layout) | +| `_app.product.$productId.tsx` | `/product/:productId` | Dynamic parameter | +| `_app.category.$categoryId.tsx` | `/category/:categoryId` | Dynamic parameter | +| `_app.cart.tsx` | `/cart` | Static route | +| `_app.account.tsx` | `/account` | Layout for account pages | +| `_app.account.orders.tsx` | `/account/orders` | Nested under account layout | + +### Naming Rules + +- **Dots (`.`)** create path segments: `_app.product.tsx` maps to `/product` +- **`$` prefix** marks dynamic parameters: `$productId` becomes `:productId` +- **`_` prefix** marks layout routes (pathless): `_app.tsx` wraps children without adding a URL segment +- **`_index`** matches the parent path exactly (index route) + +See [Route Conventions Reference](references/ROUTE-CONVENTIONS.md) for the complete naming rules. + +## Route Module Exports + +Each route file can export these values: + +### loader — Server-side data loading + +```typescript +import { createApiClients } from '@/lib/api-clients'; +import type { LoaderFunctionArgs } from 'react-router'; + +export function loader({ params, context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + return { + product: clients.shopperProducts.getProduct({ + params: { path: { id: params.productId } } + }).then(({ data }) => data), + }; +} +``` + +### action — Handle mutations (form submissions) + +```typescript +import { data, redirect } from 'react-router'; +import type { ActionFunctionArgs } from 'react-router'; + +export async function action({ request, context }: ActionFunctionArgs) { + const formData = await request.formData(); + // Handle mutation... + return data({ success: true }); +} +``` + +### default — Page component + +Components receive `loaderData` as props from the framework: + +```typescript +import { Suspense } from 'react'; +import { Await } from 'react-router'; +import { SeoMeta } from '@/components/seo-meta'; + +export default function CategoryPage({ loaderData }: { loaderData: CategoryPageData }) { + // `category` is already resolved (awaited in loader) + // `products` is a Promise (streamed) + return ( + <> + +

{loaderData.category.name}

+ }> + + {(products) => } + + + + ); +} +``` + +For SEO metadata, use the `` component inside your page component (not a `meta` export). + +## Layout Routes + +Layout routes wrap child routes with shared UI (navigation, footer, etc.): + +```typescript +// src/routes/_app.tsx — Main app layout +import { Outlet } from 'react-router'; + +export default function AppLayout() { + return ( +
+
+
+ {/* Child routes render here */} +
+
+
+ ); +} +``` + +### Adding a New Page + +1. Create a route file in `src/routes/`: + ``` + src/routes/_app.wishlist.tsx → /wishlist + ``` + +2. Export a `loader` for data and `default` for the component: + ```typescript + import { createApiClients } from '@/lib/api-clients'; + import { SeoMeta } from '@/components/seo-meta'; + import type { LoaderFunctionArgs } from 'react-router'; + + type WishlistPageData = { + wishlist: Promise; + }; + + export function loader({ context }: LoaderFunctionArgs): WishlistPageData { + const clients = createApiClients(context); + return { + wishlist: clients.shopperCustomers.getWishlist({...}).then(({ data }) => data), + }; + } + + export default function WishlistPage({ loaderData }: { loaderData: WishlistPageData }) { + return ( + <> + + }> + + {(wishlist) =>
{/* Render wishlist */}
} +
+
+ + ); + } + ``` + +## Resource Routes + +Routes that return data (no UI component): + +```typescript +// src/routes/resource.api.client.$resource.tsx +// Used by useScapiFetcher to execute SCAPI calls server-side +export function loader({ params, context }: LoaderFunctionArgs) { + // Decode params, call SCAPI, return JSON +} +``` + +## Related Skills + +- `storefront-next:sfnext-data-fetching` - What happens inside loader and action functions +- `storefront-next:sfnext-components` - Building page components with createPage +- `storefront-next:sfnext-page-designer` - Page Designer routes with regions + +## Reference Documentation + +- [Route Conventions Reference](references/ROUTE-CONVENTIONS.md) - Complete naming rules and examples diff --git a/skills/storefront-next/skills/sfnext-routing/references/ROUTE-CONVENTIONS.md b/skills/storefront-next/skills/sfnext-routing/references/ROUTE-CONVENTIONS.md new file mode 100644 index 00000000..70e0bb15 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-routing/references/ROUTE-CONVENTIONS.md @@ -0,0 +1,65 @@ +# Route Conventions Reference + +## Flat Routes File Naming + +Storefront Next uses React Router 7's flat-routes convention where dots (`.`) in file names create URL path segments. + +### Syntax Reference + +| Syntax | Purpose | Example File | URL | +|--------|---------|-------------|-----| +| `.` | Path separator | `_app.product.tsx` | `/product` | +| `$param` | Dynamic segment | `_app.product.$id.tsx` | `/product/:id` | +| `_prefix` | Pathless layout | `_app.tsx` | (no URL segment) | +| `_index` | Index route | `_app._index.tsx` | `/` (parent path) | + +### Common Route Patterns + +``` +src/routes/ +├── _app.tsx # Layout: wraps all /app routes +├── _app._index.tsx # / +├── _app.product.$productId.tsx # /product/:productId +├── _app.category.$categoryId.tsx # /category/:categoryId +├── _app.cart.tsx # /cart +├── _app.checkout.tsx # /checkout +├── _app.account.tsx # Layout: wraps /account routes +├── _app.account._index.tsx # /account +├── _app.account.orders.tsx # /account/orders +├── _app.account.orders.$orderId.tsx # /account/orders/:orderId +├── _app.search.tsx # /search +├── _auth.tsx # Layout: auth pages +├── _auth.login.tsx # /login +├── _auth.register.tsx # /register +└── resource.api.client.$resource.tsx # Resource route (no UI) +``` + +### Layout Nesting + +``` +URL: /account/orders/12345 + +Route hierarchy: + _app.tsx (layout — renders Header, Footer, ) + _app.account.tsx (layout — renders account sidebar, ) + _app.account.orders.$orderId.tsx (page — renders order details) +``` + +## Route Module Exports + +| Export | Type | When It Runs | Purpose | +|--------|------|-------------|---------| +| `loader` | Function | Server (SSR + SPA navigation) | Load data | +| `action` | Function | Server (form submissions) | Handle mutations | +| `default` | Component | Client + Server | Render UI | +| `meta` | Function | Server | Set page title, description, OG tags | +| `handle` | Object | Client + Server | Attach route metadata | +| `ErrorBoundary` | Component | Client + Server | Error UI for this route | +| `headers` | Function | Server | Set HTTP response headers | + +## Important Notes + +- All loaders run on the server, both during SSR and client-side navigation +- Route files must be `.tsx` (TypeScript with JSX) — JavaScript files are forbidden +- The `_app` prefix is a pathless layout; it does not add `/app` to the URL +- Resource routes (no default export) return data only, no rendered HTML diff --git a/skills/storefront-next/skills/sfnext-state-management/SKILL.md b/skills/storefront-next/skills/sfnext-state-management/SKILL.md new file mode 100644 index 00000000..4cdb466c --- /dev/null +++ b/skills/storefront-next/skills/sfnext-state-management/SKILL.md @@ -0,0 +1,123 @@ +--- +name: sfnext-state-management +description: Manage client-side state in Storefront Next using React context providers and feature-level Zustand stores. Use when handling basket/auth UI state, creating extension stores (for example store locator), or syncing client-visible state after server mutations. NOT for server-side data loading — see sfnext-data-fetching for loader patterns. +--- + +# State Management Skill + +This skill covers client-side state management in Storefront Next with server-first data loading. + +## Overview + +Storefront Next keeps business data on the server (`loader`/`action`) and uses client state for UX continuity. In practice, app-wide state is commonly exposed through providers (for example auth/basket), while complex extension-specific state can use Zustand. + +| State Type | Mechanism | Example | +| --------------------- | ------------------------------ | ------------------------------------------------------ | +| Server data | React Router `loader` | Product details, category listings | +| App-wide client state | React context providers | Session display state, basket snapshot/hydration state | +| Feature client state | Zustand store | Store locator modal/search state | +| Mutations | React Router `action` (server) | Add to cart, update profile | + +## Zustand Store Pattern (Feature-Level) + +```typescript +// src/extensions/store-locator/stores/store-locator-store.ts +import {createStore} from 'zustand/vanilla'; + +type StoreLocatorState = { + isOpen: boolean; + mode: 'input' | 'device'; + selectedStoreInfo: SelectedStoreInfo | null; +}; + +type StoreLocatorActions = { + open: () => void; + close: () => void; + setSelectedStoreInfo: (info: SelectedStoreInfo) => void; +}; + +type StoreLocatorStore = StoreLocatorState & StoreLocatorActions; + +export const createStoreLocatorStore = (init?: Partial) => { + return createStore()((set) => ({ + isOpen: false, + mode: 'input', + selectedStoreInfo: init?.selectedStoreInfo ?? null, + open: () => set({isOpen: true}), + close: () => set({isOpen: false}), + setSelectedStoreInfo: (selectedStoreInfo) => set({selectedStoreInfo}), + })); +}; +``` + +## Context Integration + +Expose app-level state to components via providers/hooks: + +```typescript +import { useBasket, useBasketSnapshot } from '@/providers/basket'; + +// In components +function CartIcon() { + const basket = useBasket(); + const snapshot = useBasketSnapshot(); + + const itemCount = + basket?.productItems?.length ?? + snapshot?.uniqueProductCount ?? + 0; + + return ; +} +``` + +## Post-Mutation Sync Pattern + +Keep mutations on the server and update request-context resources there: + +```typescript +import {data} from 'react-router'; +import {getBasket, updateBasketResource} from '@/middlewares/basket.server'; + +export async function action({request, context}: ActionFunctionArgs) { + const formData = await request.formData(); + const productId = formData.get('productId') as string; + + const basketResource = await getBasket(context); + const clients = createApiClients(context); + + const {data: updatedBasket} = await clients.basket.addItemToBasket({ + params: {path: {basketId: basketResource.current?.basketId ?? ''}}, + body: {productId, quantity: 1}, + }); + + // Sync basket resource in request context for current response/revalidation flow + updateBasketResource(context, updatedBasket); + + return data({success: true, basket: updatedBasket}); +} +``` + +## Best Practices + +1. **Server-first data** — Load/mutate commerce data with `loader`/`action` +2. **Provider-first app state** — Use providers for shared basket/auth UI state +3. **Scoped Zustand usage** — Use Zustand for feature-local complexity (typically extensions) +4. **Sync in server actions** — Update server basket/auth resources inside `action` handlers +5. **Keep state minimal** — Store only what cannot be derived cheaply + +## When to Use Each Mechanism + +| Scenario | Use | +| ----------------------------- | -------------------------------------------------------- | +| Product data on page load | `loader` | +| Shopping cart badge count | Basket provider hooks (`useBasket`, `useBasketSnapshot`) | +| Complex extension UI workflow | Zustand store | +| Search results | `loader` | +| Add to cart | Server `action` + resource update | + +## Related Skills + +- `storefront-next:sfnext-data-fetching` - Server-side data loading with loaders (NOT client state) +- `storefront-next:sfnext-authentication` - Auth state management +- `storefront-next:sfnext-components` - Using store data in components diff --git a/skills/storefront-next/skills/sfnext-testing/SKILL.md b/skills/storefront-next/skills/sfnext-testing/SKILL.md new file mode 100644 index 00000000..ea614ffb --- /dev/null +++ b/skills/storefront-next/skills/sfnext-testing/SKILL.md @@ -0,0 +1,184 @@ +--- +name: sfnext-testing +description: Write tests for Storefront Next using Vitest unit tests, Storybook stories, interaction tests, snapshot tests, and accessibility tests. Use when writing component tests, creating Storybook stories, running test coverage, or setting up test utilities. Covers testing-library patterns, play functions, and coverage thresholds. +--- + +# Testing Skill + +This skill covers testing patterns in Storefront Next — Vitest for unit tests and Storybook for component stories, interaction tests, and accessibility validation. + +## Unit Tests (Vitest) + +Test files live alongside source files with `.test.ts` or `.test.tsx` extension: + +```typescript +// src/components/product-card/product-card.test.tsx +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ProductCard } from './product-card'; +import { mockProduct } from '@/test-utils/mocks'; + +describe('ProductCard', () => { + it('renders product name', () => { + render(); + expect(screen.getByText(mockProduct.productName)).toBeInTheDocument(); + }); +}); +``` + +### Test Utilities + +Shared utilities in `src/test-utils/`: + +- `config.ts` — Mock configuration objects and ConfigProvider wrappers +- `context-provider-utils.ts` — Context provider helpers for testing +- `context-provider.tsx` — Test context providers + +### Running Tests + +```bash +pnpm test # Run all tests with coverage +pnpm test:ui # Open interactive Vitest UI +pnpm test:watch # Watch mode (re-run on changes) +``` + +### Coverage Requirements + +Thresholds enforced in `vitest.thresholds.ts`: + +| Metric | Minimum | +|--------|---------| +| Lines | 73% | +| Statements | 73% | +| Functions | 72% | +| Branches | 67% | + +### Testing Libraries + +- `@testing-library/react` — Component rendering and queries +- `@testing-library/jest-dom` — Custom DOM matchers (`toBeInTheDocument`, etc.) +- `@testing-library/user-event` — User interaction simulation +- `@vitest/coverage-v8` — Code coverage +- `@vitest/ui` — Interactive test UI + +## Storybook + +Every reusable component should have a `.stories.tsx` file. + +### Story Structure + +```typescript +// src/components/product-card/product-card.stories.tsx +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { within, expect } from 'storybook/test'; +import { waitForStorybookReady } from '@storybook/test-utils'; +import { ProductCard } from './product-card'; +import { ConfigProvider } from '@/config/context'; +import { mockConfig } from '@/test-utils/config'; +import { mockProduct } from '@/test-utils/mocks'; + +const meta: Meta = { + title: 'Components/ProductCard', + component: ProductCard, + tags: ['autodocs', 'interaction'], + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + product: mockProduct, + }, + play: async ({ canvasElement }) => { + await waitForStorybookReady(canvasElement); + const canvas = within(canvasElement); + await expect(canvas.getByText(mockProduct.productName)).toBeInTheDocument(); + }, +}; +``` + +See [Storybook Patterns Reference](references/STORYBOOK-PATTERNS.md) for advanced patterns. + +### Storybook Commands + +```bash +pnpm storybook # Dev server (port 6006) +pnpm build-storybook # Build static Storybook +pnpm test-storybook:snapshot # Snapshot tests +pnpm test-storybook:snapshot:update # Update snapshots +pnpm test-storybook:interaction # Interaction tests +pnpm test-storybook:a11y # Accessibility tests +pnpm generate:story-tests:coverage # Story coverage report +``` + +### Story Tags + +| Tag | Purpose | +|-----|---------| +| `autodocs` | Enable automatic documentation | +| `interaction` | Include in interaction test runs | +| `skip-a11y` | Exclude from a11y tests (use sparingly) | + +## Route Testing + +Mock loaders, actions, and React Router context: + +```typescript +// src/routes/_app.product.$productId.test.tsx +import { describe, test, expect, vi } from 'vitest'; +import { render } from '@testing-library/react'; + +vi.mock('@/components/product-view', () => ({ + default: ({ product }: any) => ( +
+
{product?.name}
+
+ ), +})); +``` + +## Test Organization + +``` +src/ +├── components/ +│ ├── product-card/ +│ │ ├── index.tsx # Component +│ │ ├── index.test.tsx # Unit tests +│ │ └── stories/ +│ │ └── index.stories.tsx # Stories +├── routes/ +│ ├── _app.product.$productId.tsx +│ └── _app.product.$productId.test.tsx +└── test-utils/ # Shared utilities + ├── config.ts + └── context-provider-utils.ts +``` + +## Best Practices + +1. **Colocate tests** — Keep test files next to source files +2. **Use test utilities** — Leverage `@/test-utils` for mocks and providers +3. **Mock external dependencies** — Use `vi.mock()` for API clients and context +4. **Test interactions** — Use `@testing-library/user-event` and Storybook play functions +5. **Test accessibility** — Use Storybook a11y addon +6. **Multiple story variants** — Create stories for Default, Loading, Error states +7. **Use viewport toolbar** — Test responsive layouts via Storybook's built-in viewport toolbar + +## Related Skills + +- `storefront-next:sfnext-components` - Component patterns that need testing +- `storefront-next:sfnext-data-fetching` - Mocking loaders and actions in tests +- `storefront-next:sfnext-page-designer` - Testing Page Designer components + +## Reference Documentation + +- [Storybook Patterns Reference](references/STORYBOOK-PATTERNS.md) - Advanced story patterns and testing diff --git a/skills/storefront-next/skills/sfnext-testing/references/STORYBOOK-PATTERNS.md b/skills/storefront-next/skills/sfnext-testing/references/STORYBOOK-PATTERNS.md new file mode 100644 index 00000000..5b92fa64 --- /dev/null +++ b/skills/storefront-next/skills/sfnext-testing/references/STORYBOOK-PATTERNS.md @@ -0,0 +1,118 @@ +# Storybook Patterns Reference + +## Story File Structure + +```typescript +// src/components/my-component/stories/index.stories.tsx +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { within, expect, userEvent } from 'storybook/test'; +import { waitForStorybookReady } from '@storybook/test-utils'; +import { MyComponent } from '../my-component'; +import { ConfigProvider } from '@/config/context'; +import { mockConfig } from '@/test-utils/config'; + +const meta: Meta = { + title: 'Components/MyComponent', + component: MyComponent, + tags: ['autodocs', 'interaction'], + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; +``` + +## Story Variants + +Create stories for different component states: + +```typescript +export const Default: Story = { + args: { title: 'Default Title' }, +}; + +export const Loading: Story = { + args: { isLoading: true }, +}; + +export const Error: Story = { + args: { error: 'Something went wrong' }, +}; + +export const Empty: Story = { + args: { items: [] }, +}; +``` + +## Interaction Tests (Play Functions) + +```typescript +export const WithInteraction: Story = { + args: { title: 'Interactive' }, + play: async ({ canvasElement }) => { + await waitForStorybookReady(canvasElement); + const canvas = within(canvasElement); + + // Assert initial state + await expect(canvas.getByText('Interactive')).toBeInTheDocument(); + + // Simulate user interaction + const button = canvas.getByRole('button'); + await userEvent.click(button); + + // Assert result + await expect(canvas.getByText('Clicked!')).toBeInTheDocument(); + }, +}; +``` + +## Decorators + +Wrap stories with required providers: + +```typescript +// Single provider +decorators: [ + (Story) => ( + + + + ), +], + +// Multiple providers +decorators: [ + (Story) => ( + + + + + + ), +], +``` + +## Responsive Testing + +Use Storybook's built-in viewport toolbar instead of creating separate Mobile/Tablet/Desktop stories. The viewport selector in the toolbar lets you test components at different screen sizes interactively. + +## Accessibility Testing + +Stories with the `autodocs` tag are automatically included in a11y test runs. Use `skip-a11y` only for stories that intentionally violate a11y rules (e.g., testing error states): + +```typescript +export const ErrorState: Story = { + tags: ['skip-a11y'], + args: { error: 'Missing required field' }, +}; +``` + +## Story Coverage + +Run `pnpm generate:story-tests:coverage` to check which components have stories and which need them. Every reusable component in `src/components/` should have a corresponding `.stories.tsx` file.