Setup Next.js 16 App Router
Jeśli zacząłeś od doswiftly init — masz cały ten setup już wygenerowany w projekcie. Tę stronę czytaj, żeby zrozumieć jak SDK wpina się w Next.js (StorefrontProvider, BFF route handlers, codegen) albo gdy konfigurujesz storefront ręcznie (self-host, własny scaffold, migracja istniejącego projektu).
Kompletny przewodnik wpięcia SDK od zera — koszyk, logowanie, BFF auth, GraphQL codegen. Pierwsza działająca strona w 15 minut.
1. Zainstaluj pakiety
pnpm add @doswiftly/storefront-sdk @doswiftly/storefront-operations next react react-dom
pnpm add -D @types/react @types/react-dom typescript @graphql-codegen/cli @graphql-codegen/client-preset
Peer dependencies: react ^18 || ^19, zustand ^5 (jeśli używasz react subpath SDK).
2. Zmienne środowiskowe (.env.local)
NEXT_PUBLIC_API_URL=https://api.doswiftly.pl
NEXT_PUBLIC_SHOP_SLUG=my-shop
Plik .env.local w template scaffoldowanym przez doswiftly init jest commitowany do repo (nie ląduje w .gitignore) — wartości są dev-friendly i niewrażliwe. SDK przyjmuje apiUrl i shopSlug jako explicit config (nie czyta env vars, nie sniffuje hostname). Te dwie zmienne są inlinowane przez Next.js przy build i czytane jako argumenty <StorefrontProvider config={{ apiUrl, shopSlug }}>.
Storefront wdrożony przez doswiftly deploy jest serwowany przez Cloudflare Worker (dispatch worker) — Worker routuje request po hostname'ie i wstrzykuje header X-Shop-Slug informational dla downstream BFF route handlers oraz audit log w backendzie. SDK tego headeru nie czyta przy konstrukcji request'u — kontrakt apiUrl + shopSlug przekazany do createStorefrontClient jest jedynym źródłem prawdy dla transportu.
Dla self-hosted deploys (Vercel, bare CF Worker bez dispatch) konfiguracja jest identyczna — wpisujesz apiUrl + shopSlug w .env.local lub doswiftly.config.ts, SDK działa bez zmian.
3. Root layout z StorefrontProvider
app/layout.tsx:
import { StorefrontProvider } from '@doswiftly/storefront-sdk/react';
import { getStorefrontClient } from '@doswiftly/storefront-sdk/react/server';
import { ShopConfigDocument } from '@/generated/graphql';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
// Server-side client — bez auth/currency middleware (brak Zustand stores na serwerze)
const serverClient = getStorefrontClient({
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
});
// Fetch minimalny payload Shop (currency setup, language setup, bot protection)
// dopasowany 1:1 do typu ShopConfig wymaganego przez <StorefrontProvider>.
// Layout nie czyta cookies — trasa może być renderowana statycznie (ISR).
const { shop } = await serverClient.query(ShopConfigDocument);
return (
<html lang={shop.defaultLanguage ?? 'pl'}>
<body>
<StorefrontProvider
config={{
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
}}
shopData={shop}
>
{children}
</StorefrontProvider>
</body>
</html>
);
}
Provider bez propsów initial* sam odtwarza sesję po twardym refreshu: odczytuje w przeglądarce czytelne cookie session-expiry (sam znacznik czasu — nigdy token) i odnawia access token w tle przez trasę BFF. Layout zostaje statyczny. Kompletny przepływ logowania: przepis Logowanie i trwała sesja.
Opcjonalny seed serwerowy (initialAccessToken z odczytu httpOnly cookie albo getInitialAuth()) usuwa nawet krótki placeholder pierwszego renderu i round-trip do /api/auth/whoami — ale odczyt cookies() w Server Component wymusza dynamiczne renderowanie całego poddrzewa layoutu. Na storefroncie ze statycznym katalogiem to zła wymiana; sięgnij po niego tylko, gdy layout i tak jest dynamiczny. Warianty i bezpieczeństwo (token wyłącznie w pamięci, nigdy localStorage): Autoryzacja klienta.
Przekazanie jakiegokolwiek propsa initial* (nawet false/null) wyłącza automatyczny odczyt cookie.
ShopConfigFields@doswiftly/storefront-operations publishuje query ShopConfig i fragment ShopConfigFields on Shop z minimalną selekcją pól zgodnych 1:1 z interfejsem ShopConfig wymaganym przez <StorefrontProvider shopData={...}> — currency setup (currencyCode, supportedCurrencies, localeToCurrencyMap), language setup (defaultLanguage, supportedLanguages), bot protection config. Bez ręcznej selekcji pól, bez drift gdy SDK doda opcjonalne pole.
Dla większej selekcji (branding, contact, business hours dla własnego UI) użyj pełnego fragmentu Shop z fragments.graphql.
4. BFF route handlers (3 pliki)
SDK eksportuje gotowe factory dla Web API (Request/Response). Te trzy route handlery domknują cały flow httpOnly cookie. Każdy ma isTrustedOrigin: trustedForwardedHostValidator żeby auth działał za reverse proxy (DoSwiftly hosting, Vercel) — bez tej opcji proxy strip'uje Host header i strict origin check zwraca 403:
app/api/auth/set-token/route.ts:
import { createSetTokenHandler, trustedForwardedHostValidator } from '@doswiftly/storefront-sdk';
export const POST = createSetTokenHandler({ isTrustedOrigin: trustedForwardedHostValidator });
app/api/auth/clear-token/route.ts:
import { createClearTokenHandler, trustedForwardedHostValidator } from '@doswiftly/storefront-sdk';
export const POST = createClearTokenHandler({ isTrustedOrigin: trustedForwardedHostValidator });
app/api/auth/whoami/route.ts:
import { createWhoamiHandler, trustedForwardedHostValidator } from '@doswiftly/storefront-sdk';
export const GET = createWhoamiHandler({
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
isTrustedOrigin: trustedForwardedHostValidator,
});
Każdy handler waliduje Origin przeciwko X-Forwarded-Host (gdy isTrustedOrigin skonfigurowane) lub strict new URL().host === Host header (fallback bez proxy). Wymaga Content-Type: application/json, ustawia HttpOnly + SameSite=Lax + Secure (w produkcji). Bez additional config.
Dla bare-metal deploys bez proxy (Next.js receives traffic directly) — usuń opcję isTrustedOrigin, strict fallback załatwi sprawę. Patrz Route handlers za reverse proxy po szczegóły.
5. Codegen ze schematu w @doswiftly/storefront-operations (bez live backendu)
@doswiftly/storefront-operations jest linked z @doswiftly/storefront-sdk (zainstalowany automatycznie razem z SDK) i publishuje schema.graphql jako raw plik. Codegen i IDE GraphQL plugin czytają go bezpośrednio — bez połączenia z backend'em.
codegen.ts:
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'node_modules/@doswiftly/storefront-operations/schema.graphql',
documents: ['./app/**/*.{ts,tsx,graphql}', './lib/**/*.{ts,tsx,graphql}'],
generates: {
'./generated/graphql.ts': {
preset: 'client',
config: { documentMode: 'string' },
},
},
};
export default config;
Dla VS Code GraphQL extension (graphql-config):
// .graphqlrc.json
{
"schema": "node_modules/@doswiftly/storefront-operations/schema.graphql",
"documents": "app/**/*.{ts,tsx,graphql}"
}
pnpm graphql-codegen --config codegen.ts
6. Pierwsza strona z koszykiem
app/products/[slug]/page.tsx:
import { Image, PriceDisplay, AddToCartButton } from '@doswiftly/storefront-sdk/react';
import { fetchProduct } from '@/lib/graphql/server';
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const { product } = await fetchProduct(slug);
// Cena pochodzi z priceRange (Money pair) — Product nie ma pola `price`.
// PriceDisplay przyjmuje minor units, więc konwertujemy major → minor (× 100).
const price = product.priceRange.minVariantPrice;
const compareAt = product.compareAtPriceRange?.minVariantPrice;
return (
<article>
<Image data={product.featuredImage} sizes="(max-width: 768px) 100vw, 50vw" priority />
<h1>{product.title}</h1>
<PriceDisplay
price={Number(price.amount) * 100}
compareAtPrice={compareAt ? Number(compareAt.amount) * 100 : undefined}
currency={price.currencyCode}
/>
{/* variants to Relay Connection — pierwszy wariant przez .nodes[0] */}
<AddToCartButton variantId={product.variants.nodes[0].id}>
Dodaj do koszyka
</AddToCartButton>
</article>
);
}
7. Global cart-expired listener (jeden raz w aplikacji)
app/_cart-expired-toast.tsx — client component osadzony w layout.tsx:
'use client';
import { useEffect } from 'react';
import { useCartManager } from '@doswiftly/storefront-sdk/react';
import { toast } from 'sonner';
export function CartExpiredToast() {
const { onExpired } = useCartManager();
useEffect(
() =>
onExpired((event) => {
if (event.reason === 'state-dependent') {
toast.error('Twój koszyk wygasł, dodaj produkty ponownie');
} else {
toast.error('Nie udało się odzyskać koszyka, spróbuj ponownie');
}
}),
[onExpired],
);
return null;
}
I dodać w layout.tsx body:
<StorefrontProvider /* ... */>
<CartExpiredToast />
{children}
</StorefrontProvider>
next.config — statyki budowane z CDN (opcjonalne, zalecane na produkcji)
Pliki budowane przez framework (JavaScript, CSS, fonty pod /_next/static/*) są wersjonowane skrótem treści i niezmienne. Możesz serwować je z CDN zamiast z origin Twojego storefrontu — szybciej dla użytkownika i mniej ruchu na serwerze storefrontu. SDK dostarcza getAssetPrefix(), który zwraca bazę CDN wstrzykiwaną przez pipeline wdrożeniowy (albo undefined w dev i na wdrożeniach bez skonfigurowanego CDN — wtedy statyki idą z origin, bez zmian).
next.config.ts:
import { getAssetPrefix } from '@doswiftly/storefront-sdk/next/config';
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
assetPrefix: getAssetPrefix(),
crossOrigin: 'anonymous',
// ...reszta konfiguracji
};
export default nextConfig;
Importuj getAssetPrefix z podścieżki @doswiftly/storefront-sdk/next/config (nie /next) — next.config jest ładowany natywnie przez Node, a ta podścieżka to pojedynczy moduł bez zależności, który rozwiązuje się tam bez bundlera.
crossOrigin: 'anonymous' jest wymagane — statyki ładują się wtedy cross-origin, a CDN wysyła odpowiedni nagłówek CORS (potrzebny m.in. dla fontów). W next dev getAssetPrefix() zwraca undefined, więc lokalny dev serwuje statyki bez zmian (fast refresh działa).
getAssetPrefix() zwraca pełną bazę CDN wstrzykniętą przez platformę, więc gdy storefront jest hostowany na własnej domenie, statyki dostają czyste URL-e (bez wewnętrznego segmentu sklepu) automatycznie — nie musisz nic konfigurować.
Powyższe dotyczy statyków budowanych (JS/CSS/fonty). Optymalizacja obrazów (<Image>, public/, importy w kodzie) działa niezależnie — patrz Obrazy. Czyste URL-e na własnej domenie obejmują oba mechanizmy — szczegóły: Obrazy → Czyste URL-e na własnej domenie.
Co działa out of the box
Po wykonaniu 7 kroków:
- ✅ Cart persistence w composite cookie
cart-id(cart_id+ sekret, SSR-visible, 30 dni) — middleware sekretu wpinany automatycznie przezStorefrontProvider - ✅ Auto-recovery wygasłych / niedostępnych koszyków (
CART_NOT_FOUND→ atomiccartCreate({ lines })dlaaddItem, bail+toast dlaupdateItem/removeItem) - ✅ Auth flow z httpOnly cookie (BFF)
- ✅ Whoami hydration po hard refresh (eliminuje flash "Sign In")
- ✅ Tree-shakeable bundle (
sideEffects: false) - ✅ Edge runtime compatible (
fetch,AbortController,URL, brak Node APIs) - ✅ Walidacja GraphQL operations w CI bez backendu (schema z
@doswiftly/storefront-operations)
Następne kroki
- Koszyk — operacje koszyka, model capability (composite cookie + sekret), recovery taxonomy. Dla odczytu koszyka po stronie serwera (SSR / edge) przekaż sekret przez
serverCartSecretMiddleware— patrz sekcja "Sekret koszyka i cookie composite" - Logowanie i sesja klienta — focused hooks (
useLogin,useLogout,useRefreshToken), BFF i bezpieczeństwo - Klient — panel klienta