Skip to content
Guides

Pages and routes

Create SSR routes with pages/, definePage, and serverApi.

The CLI turns files under pages/ into routes. Do not configure TanStack Router or Vite manually.

FileRoute
pages/index.tsx/
pages/products.index.tsx/products/
pages/products.$productSlug.tsx/products/:productSlug
pages/api/$.ts/api/*

UI pages use .tsx or .jsx. API routes use .ts and can export GET, POST, PUT, PATCH, and DELETE handlers.

Page with data

pages/products.$productSlug.tsx
import {
  definePage,
  serverApi,
  stripHtml,
  z,
  type ProductDetail,
} from "@ecomiq/storefront";

const SearchSchema = z.object({
  sellerId: z.string().uuid().optional(),
});

type ProductSearch = z.infer<typeof SearchSchema>;

export default definePage<
  ProductDetail,
  { productSlug: string },
  ProductSearch
>({
  validateSearch: (raw) => SearchSchema.parse(raw),
  loader: ({ params, search }) =>
    serverApi.getProductBySlug(params.productSlug, {
      sellerId: search.sellerId,
    }),
  meta: ({ data }) => ({
    title: data.name,
    description: stripHtml(data.description).slice(0, 155),
    ogImage: data.images[0]?.url || data.imageUrl,
  }),
  component: ({ data, settings }) => (
    <article>
      <p>{settings.store.name}</p>
      <h1>{data.name}</h1>
      <p>
        {data.price} {settings.store.currency}
      </p>
    </article>
  ),
});

loader receives { params, search, config }. component receives { data, params, search, settings }. URL search changes rerun the loader by default; use reloadOnSearch: false when handling them on the client.

definePage also accepts:

  • preload
  • loading
  • error
  • meta with title, description, ogImage, noIndex, meta, and jsonLd

Global layout

layout.tsx is optional. When present, it replaces the framework's basic layout:

layout.tsx
import { defineLayout, Link, useMenu } from "@ecomiq/storefront";

export default defineLayout(({ children, settings }) => {
  const main = useMenu("main");

  return (
    <>
      <header>
        <Link to="/">{settings.store.name}</Link>
        <nav>
          {main.map((item) => (
            <Link key={item.url} to={item.url}>
              {item.label}
            </Link>
          ))}
        </nav>
      </header>
      <main>{children}</main>
    </>
  );
});

Same-origin proxy

Customer accounts use an /api/* proxy. The starter defines it as follows:

pages/api/$.ts
import { proxyStorefrontRequest } from "@ecomiq/storefront";

type ApiHandlerArgs = {
  request: Request;
  params: { _splat?: string };
};

async function handle({ request, params }: ApiHandlerArgs) {
  return proxyStorefrontRequest(request, params._splat);
}

export const GET = handle;
export const POST = handle;
export const PUT = handle;
export const PATCH = handle;
export const DELETE = handle;

Reserved routes

The framework generates and protects these routes:

  • /checkout/ and /checkout/:checkoutId
  • /thank-you
  • /pages/:pageSlug
  • /account/, /account/addresses, and /account/orders
  • /account/orders/:orderId and /account/devices

Do not create files under pages/ that try to replace them. /account/devices is currently an informational screen: there is no endpoint for listing devices yet.

On this page