Przejdź do głównej zawartości

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:

  1. Bez transformacjiurl zwraca oryginalny obraz (bezposrednio z R2, bez przetwarzania)
  2. Z transformacjamiurl(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

PoleTypZakresDomyslnaOpis
maxWidthInt1–5760brakSzerokosc w pikselach
maxHeightInt1–5760brakWysokosc w pikselach
cropCropRegionCENTER, TOP, BOTTOM, LEFT, RIGHTbrak (fit)Region przycinania
scaleInt1–31Mnoznik DPI (retina)
preferredContentTypeImageContentTypeJPG, PNG, WEBP, AVIFWEBPFormat wyjsciowy
Konwencja transformacji obrazów + AVIF

ImageTransformInput ma pola zgodne z powszechną konwencją transformacji obrazów, plus AVIF jako dodatkowy format.

Zachowanie resize: fit vs fill

  • Bez cropfit — obraz pasuje do bounding box zachowujac proporcje. Wynikowy obraz moze byc mniejszy niz podane wymiary.
  • Z cropfill — obraz wypelnia dokladnie podane wymiary, nadmiar przycinany wg crop region.
  • Brak upscalingu — obraz nigdy nie bedzie wiekszy niz oryginal. Jesli maxWidth: 3000 a 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}
/>
);
}
wskazówka

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

  1. Zawsze ustawiaj width i height (lub fill) — zapobiega layout shift (CLS)
  2. Uzyj sizes prop — pomaga przegladarce wybrac odpowiedni srcset entry
  3. Dodaj priority do LCP image — obraz widoczny bez przewijania
  4. Uzyj alt text — dostepnosc + SEO
  5. Uzywaj transform w 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 width per wpis srcset, wiec ten sam obraz renderuje sie w rozmiarze dopasowanym do viewportu (prawdziwy responsywny srcset). Mozesz pominac transform: { 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 per srcset). Zero zmian w kodzie — import + <Image> dziala jak dotad.
  • Reszta (/_next/* poza media/ — 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).

Wersjonowanie 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:

srcNa 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=256img.twojadomena.pl/products/.../x.jpg?width=256
Obraz lokalny (public/)img.doswiftly.pl/s/{sklep}/hero.webp?width=256img.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 url juz 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.

Reczne nadpisanie (testy / niestandardowy hosting)

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;
// }