Saleor “Paper” Storefront Audit
Codebase Stats
| Metric | Value |
|---|---|
Total files in src/ | 316 |
| Lines of code (TS/TSX/JS/JSX) | ~24,500 |
| Source location | src/ (Next.js App Router) |
| Framework | Next.js 16.1.2, React 19.1.2 |
| Package manager | pnpm 10.28.1 |
| UI foundation | Radix UI + shadcn-style primitives |
| Styling | Tailwind CSS 3.4 + OKLCH CSS custom properties |
Page Routes
| Route | Description |
|---|---|
/[channel] | Homepage — featured products grid |
/[channel]/products | Product listing page with pagination, filtering, sorting |
/[channel]/products/[slug] | Product detail page with gallery, variants, add-to-cart, JSON-LD |
/[channel]/categories/[slug] | Category page — hero banner + filtered product grid |
/[channel]/collections/[slug] | Collection page — hero banner + filtered product grid |
/[channel]/search?query= | Search results page with sort and pagination |
/[channel]/cart | Full cart page with line items, totals, checkout link |
/[channel]/login | Login page (also handles set-password via query params) |
/[channel]/signup | Registration page |
/[channel]/account | Account overview — recent orders + default address |
/[channel]/account/orders | Paginated order history |
/[channel]/account/addresses | Saved addresses with add/edit/delete/set-default |
/[channel]/account/settings | Profile settings — edit name, change password, delete account |
/[channel]/pages/[slug] | CMS content pages (EditorJS) |
/checkout?checkout=ID | Multi-step checkout flow (separate layout) |
POST /api/auth/register | Account registration |
POST /api/auth/reset-password | Password reset request |
POST /api/auth/set-password | Set new password |
GET/POST /api/revalidate | Webhook-driven cache revalidation |
GET /api/og | Dynamic OpenGraph image generation |
Commerce Components
Product Card / Product List
| Component | Path | Lines |
|---|---|---|
| ProductCard | src/ui/components/plp/product-card.tsx | 145 |
| ProductGrid | src/ui/components/plp/product-grid.tsx | 15 |
| ProductList | src/ui/components/product-list.tsx | — |
| CategoryHero | src/ui/components/plp/category-hero.tsx | 83 |
| PageHeader | src/ui/components/plp/page-header.tsx | 44 |
Cart
| Component | Path | Lines |
|---|---|---|
| CartDrawer | src/ui/components/cart/cart-drawer.tsx | 372 |
| CartDrawerWrapper | src/ui/components/cart/cart-drawer-wrapper.tsx | 20 |
| CartButton | src/ui/components/cart/cart-button.tsx | 35 |
| CartContext | src/ui/components/cart/cart-context.tsx | 41 |
| Cart full page | src/app/[channel]/(main)/cart/page.tsx | 149 |
Checkout (multi-step)
| Component | Path | Lines |
|---|---|---|
| SaleorCheckout (orchestrator) | src/checkout/views/saleor-checkout/saleor-checkout.tsx | 151 |
| InformationStep | src/checkout/views/saleor-checkout/information-step.tsx | 494 |
| ShippingStep | src/checkout/views/saleor-checkout/shipping-step.tsx | 218 |
| PaymentStep | src/checkout/views/saleor-checkout/payment-step.tsx | 450 |
| ConfirmationStep | src/checkout/views/saleor-checkout/confirmation-step.tsx | 123 |
| ExpressCheckout | src/checkout/components/express-checkout/express-checkout.tsx | 70 |
| AddressCard / AddressSelector | src/checkout/components/shipping-address/ | ~6 files |
| GuestContact / SignInForm | src/checkout/components/contact/ | ~5 files |
Variant Picker
| Component | Path | Lines |
|---|---|---|
| VariantSelectionSection | src/ui/components/pdp/variant-selection/variant-selection-section.tsx | — |
| VariantSelector | src/ui/components/pdp/variant-selection/variant-selector.tsx | — |
| ColorSwatchOption | src/ui/components/pdp/variant-selection/renderers/ | — |
| SizeButtonOption | src/ui/components/pdp/variant-selection/renderers/ | — |
| VariantSectionDynamic | src/ui/components/pdp/variant-section-dynamic.tsx | 190 |
| Total pdp directory | src/ui/components/pdp/ | 565 |
Faceted Filtering
| Component | Path | Lines |
|---|---|---|
| FilterBar | src/ui/components/plp/filter-bar.tsx | 487 |
| useProductFilters | src/ui/components/plp/use-product-filters.ts | 279 |
| filter-utils.ts | src/ui/components/plp/filter-utils.ts | 252 |
| Total plp directory | src/ui/components/plp/ | 1,912 |
Search
| Component | Path | Lines |
|---|---|---|
| SearchBar | src/ui/components/nav/components/search-bar.tsx | 36 |
| SearchResults | src/ui/components/search-results.tsx | — |
| Search provider | src/lib/search/saleor-provider.ts | 103 |
Account
| Component | Path | Lines |
|---|---|---|
| AccountNav | src/ui/components/account/account-nav.tsx | 107 |
| OrderRow | src/ui/components/account/order-row.tsx | 72 |
| OrderTimeline | src/ui/components/account/order-timeline.tsx | 122 |
| AddressFormDialog | src/ui/components/account/address-form-dialog.tsx | 200 |
| EditNameForm | src/ui/components/account/edit-name-form.tsx | 102 |
| ChangePasswordForm | src/ui/components/account/change-password-form.tsx | 151 |
| DeleteAccountSection | src/ui/components/account/delete-account-section.tsx | 81 |
| Total account | src/ui/components/account/ | 1,085 |
Navigation / Layout
| Component | Path | Lines |
|---|---|---|
| Header | src/ui/components/header.tsx | 74 |
| Footer | src/ui/components/footer.tsx | 218 |
| NavLinks | src/ui/components/nav/components/nav-links.tsx | 60 |
| MobileMenu | src/ui/components/nav/components/mobile-menu.tsx | 68 |
Other Commerce UI
| Component | Path | Lines |
|---|---|---|
| AddToCart | src/ui/components/pdp/add-to-cart.tsx | 90 |
| StickyBar (mobile ATC) | src/ui/components/pdp/sticky-bar.tsx | 82 |
| ProductGallery | src/ui/components/pdp/product-gallery.tsx | 33 |
| ImageCarousel (Embla) | src/ui/components/ui/image-carousel.tsx | 194 |
| Breadcrumbs | src/ui/components/breadcrumbs.tsx | — |
| Pagination | src/ui/components/pagination.tsx | — |
UI Primitives (shadcn-style)
src/ui/components/ui/: accordion, badge, button, carousel, checkbox, dropdown-menu, image-carousel, input, label, sheet.
Feature Inventory
Faceted Filtering
Full faceted filtering via the FilterBar component (487 lines):
- Price range: Predefined buckets (Under 50-100, 200+). Server-side via
ProductFilterInput.price. - Categories: Server-side filtered by slug-to-ID resolution.
- Colors: Client-side filtered from product attributes.
- Sizes: Client-side filtered from product attributes.
- Sort: Featured, Newest, Price asc/desc, Bestselling.
- Active filter chips with remove buttons.
- Mobile filter sheet: Responsive slide-out panel.
Available on /products, /categories/[slug], /collections/[slug].
Search
Built-in Saleor GraphQL search. No Algolia, MeiliSearch, or Typesense.
- Provider:
src/lib/search/saleor-provider.tsusesSearchProductsDocumentquery. - Code comment: “Uses Saleor’s built-in GraphQL search for the demo. Replace this with Typesense/Algolia/Meilisearch for production.”
- No autocomplete — form submits to search results page on Enter.
- Sort: relevance, price-asc, price-desc, name, newest.
- Cursor-based pagination.
Checkout
Multi-step checkout (3-4 steps depending on product type):
- Information — email/contact + shipping address
- Shipping — shipping method selection (skipped for digital)
- Payment — billing address + payment method (dummy gateway:
mirumee.payments.dummy) - Confirmation — order summary
Client-side SPA using urql for GraphQL. URL-based step navigation.
Express Checkout
Decorative only. Apple Pay and Google Pay buttons render with icons but are explicitly marked as non-functional. Comment: “Currently decorative - signals that express checkout is possible.”
Image Gallery
Full carousel on PDP using Embla Carousel (194 lines):
- Horizontal swipe on mobile
- Arrow navigation on desktop (hover-reveal)
- Thumbnail strip on desktop
- Dot indicators on mobile
- Variant-specific image switching
- No zoom/lightbox
Customer Accounts
Fully implemented:
- Login (email/password via
@saleor/auth-sdk) - Register (via API route)
- Password reset (full email flow, enumeration-protected)
- Order history (paginated, status badges, timeline)
- Saved addresses (full CRUD, set default shipping/billing)
- Profile settings (edit name, change password, delete account)
- Account overview (dashboard with recent orders + default address)
SEO
- JSON-LD: Product structured data (
schema.org/Product) with pricing, availability, brand. - OG tags: Full OpenGraph + Twitter cards on all pages.
- Dynamic OG images: Via
/api/ogendpoint. - Meta tags: Per-page
generateMetadatawithseoTitle/seoDescription. - Canonical URLs: Set via
alternates.canonical. - robots.txt: Configured in root metadata.
- Sitemap: Not present.
Theming / Branding
- CSS custom properties in
src/styles/brand.cssusing OKLCH color space. - Tailwind CSS consuming CSS variables (
bg-background,text-foreground, etc.). - Dark mode via
.darkclass with complete token overrides. - Brand config in
src/config/brand.ts(site name, copyright, tagline, social). - Geist font via Next.js font system.
- Reskin difficulty: Easy. Change CSS vars in
brand.css(~120 lines) for colors, updatebrand.tsfor text.
API Integration
GraphQL Client
Two patterns:
- Server-side: Direct
fetch()withexecutePublicGraphQL/executeAuthenticatedGraphQLinsrc/lib/graphql.ts(503 lines). Request queue with rate limiting, retry with exponential backoff, typed error hierarchy. - Client-side (checkout):
urqlGraphQL client with@saleor/auth-sdk.
Queries / Mutations
- Storefront:
src/graphql/*.graphql(30+ files) → codegen tosrc/gql/ - Checkout:
src/checkout/graphql/*.graphql(4 files) → codegen tosrc/checkout/graphql/generated/ - API routes: Inline GraphQL strings
Authentication
@saleor/auth-sdkwith cookie-based token storage (access + refresh)- Server-side reads cookies via Next.js
cookies()API - Short-lived access tokens (~15min), longer-lived refresh tokens
Backend Extensibility (Saleor Apps)
Saleor extends via Saleor Apps — standalone web services that register with the Saleor instance.
- Architecture: Each App is a separate web application (typically Next.js)
- Communication: Bidirectional — Apps call Saleor GraphQL, Saleor calls Apps via webhooks
- Product lifecycle hooks:
PRODUCT_CREATED,PRODUCT_UPDATED,PRODUCT_DELETED(async webhooks). Synchronous webhooks can modify/reject during creation. - Deployment: Self-hosted as standalone service with its own URL. Requires manifest registration, app tokens with scoped permissions.
- Each integration = separate deployed service.