Cart
The cart page, whose loader returns only a title because the visitor's cart id exists in the browser, not on the server.
import { Cart, CartPage, cartLoader, CartPageProps } from '@salla.sa/twilight-theme-engine/routes/cart';In plain words
Every visitor has their own cart, and the Salla SDK keeps its id in the browser. The server that builds the page cannot know it, so Cart.loader returns just the page title and the server's HTML shows a skeleton.
Once the page runs in the browser, CartPage asks the SDK for the cart id, loads the cart and draws the items, the coupon box and the order summary. Open it with your own cart on Built-in pages.
Signature
const Cart: {
readonly id: 'cart';
readonly loader: (
ctx?: { locale?: string },
extend?: (data: CartPageProps, ctx: { params: {} }) => Record<string, unknown> | Promise<Record<string, unknown>>
) => Promise<CartPageProps>; // a promise returned by extend is ignored
readonly head: (ctx: TwilightContext, data: CartPageProps) => HeadDescriptor;
readonly Component: typeof CartPage;
};
function cartLoader(): CartPageProps; // synchronous
function CartPage(props: CartPageProps): JSX.Element;
interface CartPageProps {
page: Page; // { slug: 'cart', title }
cart?: Cart; // never set by the loader, not read by CartPage
}Try it live
import { CartPage } from '@salla.sa/twilight-theme-engine/routes/cart';
// A custom checkout step that reuses the engine's cart page as it is.
export function CheckoutCart() {
return <CartPage page={{ slug: 'cart', title: 'Cart' }} />;
}
Example
import { CartPage } from '@salla.sa/twilight-theme-engine/routes/cart';
// A custom page that reuses the engine's cart as it is.
export function CheckoutCart() {
return <CartPage page={{ slug: 'cart', title: 'Your bag' }} />;
}
How it behaves
URL:
/{-$locale}/cart. The generated route showsCartSkeletonwhile loading (see Route modules for the file).cartLoader()returns{ page: { slug: 'cart', title } }with thecommon.titles.carttranslation (orCart). It reads the i18n instance from the router's request context, so call it from a loader.headsets the title and hreflang alternates, plus a description only whenpage.descriptionis set, which the loader never does. There is no canonical.CartPageis not lazy. In the browser it readsloyalty.queries.points()at once and, after mounting, callswindow.Salla.cart.api.getCurrentCartId()and then loadscart.queries.detail(id)with TanStack Query. It rendersCartSkeletonuntil the id and the cart arrive; an empty cart showsNoContentwith a link home.Hook slots:
cart:start,cart:items.start(context{ cartItems }),cart:items.endandcart:end. It also renders Salla's conditional-offer and offer web components,SeoCartWidgetfor the analytics events, andCartSummarywith the store's coupon setting.
Gotchas
Without the Salla SDK (
window.Sallamissing, for example when its script is blocked) the cart id is never requested and the skeleton stays forever.Cart.loaderignores anextendthat returns a promise: it merges only a plain object (extra instanceof Promise ? {} : extra), with no warning. Return an object synchronously, or callCart.loaderinside your own async loader and spread the result.docs/08-page-customization.md shows a cart route whose loader fetches the cart (
loader: async () => ({ cart: await getCart() })). On the server there is no cart id to fetch; keep loading the cart in the browser, asCartPagedoes.
Related
One line of the cart page: picture, name, prices, offers, a quantity input and a delete button that change the shopper's real cart.
CartSummaryThe cart page sidebar: free-shipping bar, loyalty and gift widgets, totals, a coupon box and the Complete Order button.
Forking a built-in pageTake over one built-in page: point its path at your own route file, reuse the module's loader and head, and swap or wrap its Component.
useCouponApplies or removes a discount coupon on the shopper's cart through the Salla SDK, with loading and error state.
Source and docs
- Engine source:
packages/theme-engine/src/routes/cart/index.tsx