Obrazy
Wszystkie obrazy w storefroncie sa automatycznie zoptymalizowane — zmniejszane, konwertowane do WebP i serwowane przez globalny CDN. Nie musisz nic konfigurowac.
Jak to dziala
GraphQL API zwraca gotowe do uzycia URL-e CDN. Mozesz zrobic dwie rzeczy:
- Bez transformacji —
urlzwraca oryginalny obraz (bezposrednio z R2, bez przetwarzania) - Z transformacjami —
url(transform: { maxWidth: 800 })zwraca URL z query params, ktory CDN/imgproxy automatycznie przetwarza
Twoj serwer Next.js nigdy nie przetwarza obrazow — zero CPU, zero Sharp. Dziala na CF Workers.
Transformacje w GraphQL
Argument transform na polu url typu Image — zgodny z powszechną konwencją transformacji obrazów w Storefront API, plus AVIF.
query ProductImage {
product(handle: "classic-tee") {
featuredImage {
# Gotowy URL CDN z resize — po prostu uzyj go w <img> lub <Image>
url(transform: { maxWidth: 800 })
altText
width
height
}
}
}
Odpowiedz:
{
"url": "https://img.doswiftly.pl/s/shop-uuid/products/1/abc.jpg?width=800",
"altText": "Classic Tee front",
"width": 1920,
"height": 1080
}
Wiele rozmiarow w jednym query (aliasy)
Uzyj GraphQL field aliases aby pobrac wiele rozmiarow jednoczesnie:
query ProductImages {
product(handle: "classic-tee") {
featuredImage {
thumb: url(transform: { maxWidth: 200 })
medium: url(transform: { maxWidth: 800 })
hero: url(transform: { maxWidth: 1600 })
altText
}
}
}
Pola ImageTransformInput
| Pole | Typ | Zakres | Domyslna | Opis |
|---|---|---|---|---|
maxWidth | Int | 1–5760 | brak | Szerokosc w pikselach |
maxHeight | Int | 1–5760 | brak | Wysokosc w pikselach |
crop | CropRegion | CENTER, TOP, BOTTOM, LEFT, RIGHT | brak (fit) | Region przycinania |
scale | Int | 1–3 | 1 | Mnoznik DPI (retina) |
preferredContentType | ImageContentType | JPG, PNG, WEBP, AVIF | WEBP | Format wyjsciowy |
ImageTransformInput ma pola zgodne z powszechną konwencją transformacji obrazów, plus AVIF jako dodatkowy format.
Zachowanie resize: fit vs fill
- Bez
crop→ fit — obraz pasuje do bounding box zachowujac proporcje. Wynikowy obraz moze byc mniejszy niz podane wymiary. - Z
crop→ fill — obraz wypelnia dokladnie podane wymiary, nadmiar przycinany wgcropregion. - Brak upscalingu — obraz nigdy nie bedzie wiekszy niz oryginal. Jesli
maxWidth: 3000a oryginal ma 1920px, otrzymasz 1920px.
Przyklady uzycia
Plain <img>
function ProductCard({ product }) {
return (
<img
src={product.featuredImage.url}
alt={product.featuredImage.altText || product.title}
width={640}
height={640}
/>
);
}
Gdzie query pobiera url(transform: { maxWidth: 640 }).
Next.js <Image>
import Image from 'next/image';
function ProductCard({ product }) {
return (
<Image
src={product.featuredImage.url}
width={640}
height={640}
alt={product.featuredImage.altText || product.title}
/>
);
}
Z podpietym loaderem SDK (sekcja „Loader next/image" nizej) <Image> routuje obrazy przez CDN — produkty dostaja width per wpis srcset, a obrazy public/ sa optymalizowane. Bez loadera URL z GraphQL i tak jest gotowy do uzycia bezposrednio.
og:image (social media preview)
Uzyj preferredContentType: JPG — wiekszos platform social media lepiej obsluguje JPEG:
query OgImage {
product(handle: "classic-tee") {
featuredImage {
ogUrl: url(transform: { maxWidth: 1200, maxHeight: 630, crop: CENTER, preferredContentType: JPG })
}
}
}
<meta property="og:image" content={product.featuredImage.ogUrl} />
ProductImage — komponent z error handling
Template dostarcza <ProductImage> z wbudowanym placeholder i error handling:
import { ProductImage } from '@/components/product/product-image';
// Karta produktu
<ProductImage
image={product.featuredImage}
alt={product.title}
fill
priority={index < 4}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
// Miniaturka
<ProductImage
image={variant.image}
width={80}
height={80}
/>
Gdy obraz nie zaladuje sie lub brakuje URL — wyswietla placeholder (ikona obrazka).
Responsive images
Uzyj aliasow GraphQL z sizes prop aby przegladarka wybrala optymalny rozmiar:
<Image
src={product.featuredImage.url}
width={1200}
height={800}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
alt={product.title}
/>
Wskazowki wydajnosci
- Zawsze ustawiaj
widthiheight(lubfill) — zapobiega layout shift (CLS) - Uzyj
sizesprop — pomaga przegladarce wybrac odpowiedni srcset entry - Dodaj
prioritydo LCP image — obraz widoczny bez przewijania - Uzyj
alttext — dostepnosc + SEO - Uzywaj
transformw query — CDN cachuje gotowe warianty, szybciej niz resize client-side
{/* Hero — priorytet LCP */}
<Image
src={product.featuredImage.heroUrl}
width={1600}
height={900}
priority
sizes="100vw"
alt={product.title}
/>
ThumbHash — instant blur placeholder
Każdy obraz produktu ma pole thumbhash — perceptualny hash (~40 znaków base64) generowany przy uploadzie. Dekoduj go na kliencie aby wyświetlić rozmyty podgląd zanim obrazek się załaduje.
SDK eksportuje decoder:
import { thumbHashToDataURL } from '@doswiftly/storefront-sdk';
// GraphQL zwraca thumbhash jako string:
const blurUrl = thumbHashToDataURL(image.thumbhash);
// → "data:image/bmp;base64,..." (instant, ~32x32px rozmyty podgląd)
Z Next.js Image
import Image from 'next/image';
import { thumbHashToDataURL } from '@doswiftly/storefront-sdk';
function ProductCard({ image }) {
const blurUrl = thumbHashToDataURL(image.thumbhash);
return (
<Image
src={image.url}
alt={image.altText || ''}
fill
placeholder={blurUrl ? 'blur' : undefined}
blurDataURL={blurUrl}
/>
);
}
Template <ProductImage> robi to automatycznie — nie musisz obsługiwać thumbhash ręcznie.
Jak to działa
Upload obrazka → backend generuje thumbhash (Sharp resize 100x100 → DCT encode, ~2ms)
↓
zapisuje w DB (product_images.thumbhash)
↓
GraphQL zwraca → { url: "cdn.../product.jpg?width=800", thumbhash: "3OcRJY..." }
↓
Template → thumbHashToDataURL("3OcRJY...") → data:image/bmp;base64,...
→ <Image blurDataURL={...} /> → instant kolorowy blur → fade do prawdziwego obrazka
AVIF auto-negotiation
Nie musisz podawać preferredContentType w transform. imgproxy automatycznie serwuje:
- AVIF gdy browser wysyła
Accept: image/avif(Chrome 85+, Firefox 93+, Safari 16.4+) - WEBP jako fallback (97%+ browserów)
- JPEG jako ostateczny fallback
To działa na poziomie CDN — zero kodu po Twojej stronie. Podaj preferredContentType tylko gdy potrzebujesz konkretnego formatu (np. JPG dla og:image).
Loader next/image — optymalizacja public/
Obrazy produktow z GraphQL sa juz gotowymi URL-ami CDN. Obrazy z Twojego katalogu public/ (logo, hero, banery) domyslnie ida do przegladarki bez optymalizacji — w pelnym rozmiarze. Loader z SDK zalatwia jedno i drugie naraz.
createImageLoader() to gotowy loader dla next.config (images.loaderFile). Po podpieciu kazdy <Image> przechodzi przez CDN, rozgaleziajac po typie src:
- Obraz produktu (URL CDN z GraphQL) — loader ustawia
widthper wpissrcset, wiec ten sam obraz renderuje sie w rozmiarze dopasowanym do viewportu (prawdziwy responsywnysrcset). Mozesz pominactransform: { maxWidth }w query — loader przejmuje szerokosc. - Obraz
public/(sciezka root-relative, np./hero.webp) — routowany przez CDN: resize + negocjacja formatu (AVIF/WEBP). - Obraz importowany w kodzie (
import hero from './hero.webp') — rowniez optymalizowany. Framework pakuje go pod/_next/static/media/; loader routuje takie obrazy przez CDN (resize persrcset). Zero zmian w kodzie —import+<Image>dziala jak dotad. - Reszta (
/_next/*pozamedia/— kod JS/CSS, zewnetrzne URL-e,data:, SVG) — bez zmian.
Setup
// lib/image-loader.ts — zero argumentow
import { createImageLoader } from '@doswiftly/storefront-sdk/next';
export default createImageLoader();
// next.config.ts
const nextConfig = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.ts',
// ogranicz liczbe generowanych szerokosci (mniej transformacji, lepszy cache)
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
export default nextConfig;
Loader czyta NEXT_PUBLIC_SHOP_ID, NEXT_PUBLIC_DEPLOYMENT_COMMIT i NEXT_PUBLIC_IMGPROXY_BASE — platforma wstrzykuje je przy buildzie/deployu. Gdy ich brak (np. lokalny doswiftly dev, gdzie obrazy public/ serwuje serwer deweloperski), loader zostawia lokalne public/ bez zmian; produkty, jako absolutne URL-e CDN, dzialaja zawsze. <Image quality> jest pomijane — jakosc ustawiana jest po stronie serwera (format AVIF/WebP nadal negocjowany).
public/Loader dokleja do URL znacznik wersji deploya (cache-bust), wiec po podmianie obrazu public/ pod ta sama nazwa nowy deploy serwuje swieza wersje — zero stale cache.
Co trafia na CDN obrazow (routing)
Loader decyduje po ksztalcie sciezki src, nie po katalogu zrodlowym:
src | Na CDN obrazow? |
|---|---|
| Obraz produktu (gotowy URL CDN z GraphQL) | ✅ Tak — width nadpisywany per wpis srcset |
public/ root-relative (/hero.webp) | ✅ Tak → s/{shopId}/public/hero.webp?width=...&v=... |
public/ w podkatalogu (/images/hero.webp, /icons/x.png) | ✅ Tak — sciezka podkatalogu zachowana (s/{shopId}/public/images/hero.webp) |
Obraz importowany w kodzie (import x from './x.png') | ✅ Tak → s/{shopId}/_next/static/media/x.<hash>.png?width=... (framework pakuje go pod /_next/static/media/, loader routuje przez CDN) |
Pozostałe /_next/* (kod JS/CSS, np. /_next/static/chunks/...) lub /_nuxt/, /_astro/, /_app/ | ❌ Nie — build frameworka, juz zhashowany; nigdy nie idzie na CDN obrazow |
Zewnetrzny URL (https://...) lub protocol-relative (//host/...) | ❌ Nie — bez zmian |
data: URI lub rozszerzenie nie-obrazowe (.pdf, .svg, .ico) | ❌ Nie — bez zmian |
Zasada: na CDN obrazow ida obrazy rastrowe — z public/, z importow w kodzie (/_next/static/media/) oraz produktowe (z GraphQL). Loader decyduje po ksztalcie src i rozszerzeniu pliku, wiec kod frameworka (JS/CSS) nigdy nie trafia na CDN obrazow — nawet pod /_next/static/media/ przechodza wylacznie pliki obrazow. Pozostale /_next/*, zewnetrzne URL-e, data: i SVG (wektory nie sa rasteryzowane) zostaja nietkniete.
Czyste URL-e na wlasnej domenie
Gdy storefront jest hostowany na wlasnej domenie sklepu, platforma moze serwowac obrazy i statyki z czystych URL-i — bez wewnetrznego segmentu identyfikujacego sklep:
| Domyslnie (host platformy) | Wlasna domena (czysty URL) | |
|---|---|---|
| Obraz produktu (z API) | img.doswiftly.pl/s/{sklep}/products/.../x.jpg?width=256 | img.twojadomena.pl/products/.../x.jpg?width=256 |
Obraz lokalny (public/) | img.doswiftly.pl/s/{sklep}/hero.webp?width=256 | img.twojadomena.pl/hero.webp?width=256 |
To zachowanie jest w pelni automatyczne — platforma sygnalizuje je przy buildzie (zmienna NEXT_PUBLIC_ASSET_CLEAN_URL), a loader sam pomija segment sklepu w budowanych URL-ach. Po stronie magazynu nic sie nie zmienia (pliki sa nadal odseparowane per sklep) — czystszy jest tylko publiczny adres. Nie musisz nic robic: loader z createImageLoader() przelacza sie samoczynnie zaleznie od tego, jak wdrozono storefront.
Dotyczy to obu rodzajow obrazow, kazdego inna droga:
- Obrazy produktow (z GraphQL API) — gdy storefront dziala na wlasnej domenie, API zwraca
urljuz wskazujacy host sklepu. Nic nie konfigurujesz — gotowy URL z API wskazuje wlasciwy host. - Obrazy lokalne (
public/, importy w kodzie) — loader buduje czysty URL na hoscie sklepu.
Loader dopasowuje rozmiar (width per wpis srcset) niezaleznie od hosta — tak samo dla hosta platformy i hosta sklepu. Rozpoznaje URL produktu na obu hostach, wiec przelaczenie domeny nie wymaga zmian w kodzie.
Domyslnie loader czyta sygnal czystych URL-i ze srodowiska. Mozesz go wymusic jawnie przez opcje cleanUrl — przydatne w testach lub przy nietypowym hostingu:
// lib/image-loader.ts
import { createImageLoader } from '@doswiftly/storefront-sdk/next';
export default createImageLoader({ cleanUrl: true });
buildImageLoaderUrl przyjmuje to samo pole w configu, gdy budujesz URL-e recznie (sekcja nizej).
Recznie budowane URL-e (poza <Image>)
Loader dotyczy tylko <Image>. Dla teł CSS lub surowego <img> zbuduj URL czystym helperem buildImageLoaderUrl:
import { buildImageLoaderUrl } from '@doswiftly/storefront-sdk';
const url = buildImageLoaderUrl(
{ shopId: '...', version: '...' },
{ src: '/hero.webp', width: 1280 },
);
Typy SDK
SDK eksportuje typ ImageData i decoder thumbHashToDataURL:
import { type ImageData, thumbHashToDataURL } from '@doswiftly/storefront-sdk';
interface ProductCardProps {
image: ImageData | null;
}
// ImageData:
// {
// url: string;
// altText?: string | null;
// width?: number | null;
// height?: number | null;
// id?: string | null;
// thumbhash?: string | null;
// }