Agent Skill · Salesforce Commerce Cloud

sfnext-routing

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.

Provider: Salesforce Commerce Cloud Path in repo: skills/storefront-next/skills/sfnext-routing/SKILL.md

Skill body

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

See Route Conventions Reference for the complete naming rules.

Route Module Exports

Each route file can export these values:

loader — Server-side data loading

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)

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:

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 (
        <>
            <SeoMeta
                title={loaderData.category.name}
                description={loaderData.category.pageDescription}
            />
            <h1>{loaderData.category.name}</h1>
            <Suspense fallback={<ProductGridSkeleton />}>
                <Await resolve={loaderData.products}>
                    {(products) => <ProductGrid products={products} />}
                </Await>
            </Suspense>
        </>
    );
}

For SEO metadata, use the <SeoMeta /> component inside your page component (not a meta export).

Layout Routes

Layout routes wrap child routes with shared UI (navigation, footer, etc.):

// src/routes/_app.tsx — Main app layout
import { Outlet } from 'react-router';

export default function AppLayout() {
    return (
        <div>
            <Header />
            <main>
                <Outlet />  {/* Child routes render here */}
            </main>
            <Footer />
        </div>
    );
}

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:
    import { createApiClients } from '@/lib/api-clients';
    import { SeoMeta } from '@/components/seo-meta';
    import type { LoaderFunctionArgs } from 'react-router';
    
    type WishlistPageData = {
        wishlist: Promise<Wishlist>;
    };
    
    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 (
            <>
                <SeoMeta title="My Wishlist" />
                <Suspense fallback={<WishlistSkeleton />}>
                    <Await resolve={loaderData.wishlist}>
                        {(wishlist) => <div>{/* Render wishlist */}</div>}
                    </Await>
                </Suspense>
            </>
        );
    }
    

Resource Routes

Routes that return data (no UI component):

// 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
}

Reference Documentation