Przejdź do głównej zawartości

Strona marki

Framework: React / Next.js
Pobieranie danych jest przenośne — w blokach z przykładami zakładka Raw pokazuje czysty fetch działający w dowolnym frameworku.
UI (komponenty z hookami i JSX) jest specyficzne dla React / Next.js. W innym frameworku weź zapytanie z zakładki Raw i napisz własny widok.

Co zbudujesz

Stronę marki (/brands/[handle]): nagłówek z logo, nazwą i opisem marki, metadane SEO oraz listę produktów tej marki — wszystko jednym zapytaniem Brand. Plus indeks marek (/brands) posortowany po liczbie produktów.

Encja Brand to strona marki pobierana po handle. Nie myl jej z filtrem ProductFilter.brand (zawężanie listy produktów) ani z facetem productFilters.brands (lista marek w sidebarze) — różnicę opisuje Zapytania — Marki.

Wymagania

Krok 1 — Pobierz markę po handle

handle to slug z adresu URL (np. /brands/acme). Zapytanie Brand zwraca pola strony marki (nazwa, logo, opis, SEO) i paginowaną listę produktów marki w jednym round-tripie. To operacja raw-only (brak gotowego hooka) — wybierz kontekst renderowania:

Brak gotowego helpera SDK dla tej operacji — użyj raw operation (działa w każdym frameworku).

// Działa w dowolnym frameworku (Vue, Svelte, vanilla JS, Node, Edge)
const QUERY = `query Brand($id: ID, $handle: String, $productsFirst: Int = 20, $productsAfter: String, $productsSortKey: ProductSortKeys = BEST_SELLING, $productsFilters: [ProductFilter!]) {
brand(id: $id, handle: $handle) {
...Brand
products(
first: $productsFirst
after: $productsAfter
sortKey: $productsSortKey
filters: $productsFilters
) {
edges {
node {
...ProductCard
}
cursor
}
nodes {
...ProductCard
}
pageInfo {
...PageInfo
}
totalCount
}
}
}

fragment Brand on Brand {
id
handle
name
description
logo {
...ImageCard
}
seo {
title
description
}
productCount
}

fragment ImageCard on Image {
id
url(transform: {maxWidth: 800})
altText
width
height
thumbhash
}

fragment PageInfo on PageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}

fragment ProductCard on Product {
id
handle
title
vendor
categories {
id
handle
name
}
isAvailable
averageRating
reviewCount
tags
featuredImage {
...ImageCard
}
priceRange {
minVariantPrice {
...Money
}
maxVariantPrice {
...Money
}
}
compareAtPriceRange {
minVariantPrice {
...Money
}
maxVariantPrice {
...Money
}
}
priceReduction {
...PriceReduction
}
}

fragment Money on Money {
amount
currencyCode
}

fragment PriceReduction on PriceReductionInfo {
lowestPriorPrice {
...Money
}
reductionStartedAt
referenceWindowStartedAt
referenceWindowTruncated
percentageOffLowestPrior
label
}`;

const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, variables: { id: '…', handle: '…' }, }),
});
const { data } = await res.json();

Zwraca null, gdy marka nie istnieje lub została zarchiwizowana — obsłuż to jako 404.

Krok 2 — Rozpakuj dane marki i produkty

Pole products na marce to Relay Connection, nie tablica — wyciągnij listę przez .nodes (skrót równoważny edges.map(e => e.node)):

const brand = data.brand;
if (!brand) notFound(); // marka nie istnieje / zarchiwizowana

const items = brand.products.nodes; // produkty marki
const { hasNextPage, endCursor } = brand.products.pageInfo; // paginacja „pokaż więcej"
const total = brand.products.totalCount; // łączna liczba produktów marki
Connection ≠ tablica

brand.products to Relay Connection — indeksowanie ([0]) zwróci undefined. Zawsze rozpakuj przez .nodes (lub .edges). Listę produktów stronicujesz przez zmienne $productsFirst / $productsAfter, a kolejność przez $productsSortKey.

Krok 3 — Widok strony marki (React)

Nagłówek marki (logo + nazwa + opis) i siatka jej produktów. <title> i meta description bierzesz wprost z brand.seo — pole jest gotowe do renderu, bo pusty meta tytuł zastępuje nazwa marki, a pusty meta opis jej opis (jako czysty tekst, przycięty do długości pokazywanej w wynikach):

function BrandPage({ brand }) {
const items = brand.products.nodes;

return (
<div>
<header>
{brand.logo && (
<img src={brand.logo.url} alt={brand.logo.altText ?? brand.name} />
)}
<h1>{brand.name}</h1>
<span>{brand.productCount} produktów</span>
{brand.description && (
<div dangerouslySetInnerHTML={{ __html: brand.description }} />
)}
</header>

<div className="grid grid-cols-4 gap-4">
{items.map((item) => (
<a key={item.id} href={`/products/${item.handle}`}>
{item.title}
</a>
))}
</div>
</div>
);
}

Uwaga: brand.description to HTML — wstaw przez dangerouslySetInnerHTML (lub wcześniej sanityzuj). Pole productCount to gotowy licznik do etykiety „N produktów" — nie musisz liczyć po stronie klienta.

Krok 4 — Indeks marek posortowany po liczbie produktów

Lista aktywnych marek (/brands) to operacja Brands — również raw-only:

Brak gotowego helpera SDK dla tej operacji — użyj raw operation (działa w każdym frameworku).

// Działa w dowolnym frameworku (Vue, Svelte, vanilla JS, Node, Edge)
const QUERY = `query Brands($first: Int = 20, $after: String, $query: String, $sortKey: BrandSortKeys = NAME, $reverse: Boolean = false) {
brands(
first: $first
after: $after
query: $query
sortKey: $sortKey
reverse: $reverse
) {
edges {
node {
...Brand
}
cursor
}
pageInfo {
...PageInfo
}
totalCount
}
}

fragment Brand on Brand {
id
handle
name
description
logo {
...ImageCard
}
seo {
title
description
}
productCount
}

fragment ImageCard on Image {
id
url(transform: {maxWidth: 800})
altText
width
height
thumbhash
}

fragment PageInfo on PageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}`;

const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, variables: { first: 20, after: '…' }, }),
});
const { data } = await res.json();

Aby pokazać najpopularniejsze marki najpierw, posortuj po PRODUCT_COUNT malejąco:

{ "first": 12, "sortKey": "PRODUCT_COUNT", "reverse": true }

sortKey przyjmuje NAME (domyślny), PRODUCT_COUNT, CREATED_AT, UPDATED_AT; reverse: true odwraca kolejność. Opcjonalny query filtruje po nazwie marki (np. pole „Szukaj marki").

Alternatywa — produkty marki przez filtr

Jeśli masz już ekran listy produktów (z paginacją i facetami), nie musisz pobierać produktów przez Brand.products — zawęź istniejące zapytanie products filtrem marki po handle:

query ProductsByBrand($handle: String!) {
products(first: 20, filters: [{ brand: { handle: $handle } }]) {
nodes { id title handle }
pageInfo { hasNextPage endCursor }
}
}

Kiedy co wybrać: Brand.products — gdy budujesz stronę marki i w tym samym zapytaniu potrzebujesz nazwy / logo / opisu / SEO marki. Filtr products(filters:) — gdy reużywasz istniejącego ekranu listy z sidebarem facetów. Filtr i facet opisuje Produkty — filtrowanie po marce.

Typy

Renderowane ze schematu GraphQL — nigdy nie rozjeżdżają się z API:

Brand

Canonical product brand entity (e.g. Funko, Nike). Distinct from ShopBrand (shop branding metadata).

PoleTypOpis
descriptionStringBrand description. Returns ready-to-render HTML by default; pass `format: TEXT` for plain text or `format: JSON` for the structured document. Null when the merchant has not provided one.
handleString!URL-friendly handle (e.g. "funko" → /brands/funko). Stable per-shop.
idID!Brand unique identifier
logoImageBrand logo image. Use `logo { url(transform: { maxWidth: 200 }) altText }` for responsive sizing on brand tiles, breadcrumbs, and brand landing pages.
nameString!Brand display name (e.g. "Funko", "Nintendo")
productCountInt!Number of active products carrying this brand. Use for "N products" labels on brand tiles.
productsProductConnection!Products carrying this brand
seoSEOSEO metadata (title, description) for the brand landing page. Null when not configured.
updatedAtDateTime!Last update date (ISO 8601).

BrandConnection

BrandConnectionobjectPełna referencja →

Paginated brand connection

PoleTypOpis
edges[BrandEdge!]!Brand edges
pageInfoPageInfo!Pagination info
totalCountInt!Total count of brands

Powiązane