Настройка Shopify Hydrogen (React Storefront)
Hydrogen — официальный React-фреймворк от Shopify для headless commerce. Построен на Remix, оптимизирован для работы со Storefront API, деплоится на Oxygen (CDN-хостинг Shopify) или любой совместимый с Node.js хостинг.
Почему Hydrogen, а не просто Next.js
Next.js + Storefront API — рабочий вариант. Hydrogen добавляет:
- Oxygen-деплой — бесплатный глобальный edge-хостинг, встроенный в Shopify (от плана Basic). Нет отдельной инфраструктуры для деплоя.
-
Remix routing — вложенные роуты, server-side data loading через
loader, optimistic UI черезaction -
Shopify-специфичные примитивы —
ShopifyProvider, хуки для корзины, аналитики, кеш-стратегии - Cache API — управление кешированием на уровне Cloudflare Workers
- Streaming SSR — Progressive HTML rendering из коробки
Инициализация проекта
npm create @shopify/hydrogen@latest
# Выброр: Demo Store / Hello World
# Деплой: Oxygen / Self-hosted
cd my-hydrogen-app
npm install
npm run dev
Переменные окружения (.env):
SHOPIFY_STORE_DOMAIN=my-store.myshopify.com
SHOPIFY_STOREFRONT_ACCESS_TOKEN=abc123...
SHOPIFY_PUBLIC_STORE_DOMAIN=my-store.myshopify.com
SESSION_SECRET=random-secret-string
Структура проекта
hydrogen-app/
├── app/
│ ├── components/ # UI компоненты
│ ├── lib/ # утилиты, Shopify-клиент
│ ├── routes/ # файловый роутинг Remix
│ │ ├── _index.tsx # главная страница
│ │ ├── products.$handle.tsx # страница продукта
│ │ ├── collections.$handle.tsx
│ │ └── cart.tsx
│ ├── styles/ # глобальные CSS
│ └── root.tsx # корневой layout
├── public/
├── server.ts # Oxygen/Node entry point
└── vite.config.ts
Роут продукта с loader
// app/routes/products.$handle.tsx
import { json, type LoaderFunctionArgs } from '@shopify/remix-oxygen';
import { useLoaderData, type MetaFunction } from '@remix-run/react';
import { getSelectedProductOptions, Analytics } from '@shopify/hydrogen';
import { AddToCartButton } from '~/components/AddToCartButton';
const PRODUCT_QUERY = `#graphql
query Product($handle: String!, $country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
product(handle: $handle) {
id
title
handle
descriptionHtml
options {
name
values
}
selectedVariant: variantBySelectedOptions(
selectedOptions: $selectedOptions
ignoreUnknownOptions: true
caseInsensitiveMatch: true
) {
id
availableForSale
price {
amount
currencyCode
}
compareAtPrice {
amount
currencyCode
}
image {
url
altText
width
height
}
}
variants(first: 250) {
nodes {
id
availableForSale
selectedOptions { name value }
price { amount currencyCode }
}
}
}
}
` as const;
export async function loader({ params, request, context }: LoaderFunctionArgs) {
const { handle } = params;
const { storefront } = context;
const selectedOptions = getSelectedProductOptions(request);
const { product } = await storefront.query(PRODUCT_QUERY, {
variables: {
handle,
selectedOptions,
country: storefront.i18n.country,
language: storefront.i18n.language,
},
cache: storefront.CacheShort(), // кеш на 1 минуту
});
if (!product) throw new Response('Not Found', { status: 404 });
return json({ product });
}
export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
{ title: data?.product.title },
{ name: 'description', content: data?.product.descriptionHtml.slice(0, 160) },
];
};
export default function ProductPage() {
const { product } = useLoaderData<typeof loader>();
const { selectedVariant } = product;
return (
<div className="product">
<h1>{product.title}</h1>
<ProductPrice
price={selectedVariant?.price}
compareAtPrice={selectedVariant?.compareAtPrice}
/>
<AddToCartButton
disabled={!selectedVariant?.availableForSale}
variantId={selectedVariant?.id}
>
{selectedVariant?.availableForSale ? 'В корзину' : 'Нет в наличии'}
</AddToCartButton>
{/* Аналитика — автоматически отправляет product viewed event */}
<Analytics.ProductView
data={{
products: [{
id: product.id,
title: product.title,
price: selectedVariant?.price.amount ?? '0',
vendor: '',
variantId: selectedVariant?.id ?? '',
variantTitle: selectedVariant?.selectedOptions.map(o => o.value).join(' / ') ?? '',
quantity: 1,
}],
}}
/>
</div>
);
}
Стратегии кеширования
Hydrogen предоставляет встроенные стратегии кеша на уровне Cloudflare Workers:
// server.ts — настройка стратегий
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const { storefront } = createStorefrontClient({
cache: await caches.open('hydrogen'),
waitUntil: (p) => ctx.waitUntil(p),
// ...
});
}
};
// В loader-ах:
storefront.CacheShort() // 1 минута (volatile данные: корзина)
storefront.CacheLong() // 1 час (продукты, коллекции)
storefront.CacheCustom({ // кастомный TTL
mode: 'public',
maxAge: 300,
staleWhileRevalidate: 600,
})
storefront.CacheNone() // без кеша (персонализированный контент)
Корзина через CartProvider
// app/components/AddToCartButton.tsx
import { CartForm } from '@shopify/hydrogen';
export function AddToCartButton({
variantId,
quantity = 1,
disabled,
children,
}: {
variantId: string;
quantity?: number;
disabled?: boolean;
children: React.ReactNode;
}) {
return (
<CartForm
route="/cart"
inputs={{ lines: [{ merchandiseId: variantId, quantity }] }}
action={CartForm.ACTIONS.LinesAdd}
>
{(fetcher) => (
<button
type="submit"
disabled={disabled || fetcher.state !== 'idle'}
>
{fetcher.state !== 'idle' ? 'Добавляем...' : children}
</button>
)}
</CartForm>
);
}
// app/routes/cart.tsx — обработчик действий корзины
import { cartAction } from '~/lib/cart';
export async function action({ request, context }: ActionFunctionArgs) {
return cartAction({ request, context });
}
Деплой на Oxygen
# Линковка с Shopify store
npx shopify hydrogen link
# Деплой на Oxygen
npx shopify hydrogen deploy
# Просмотр логов
npx shopify hydrogen logs
Oxygen использует Cloudflare Workers — глобальная сеть, ближайший edge к пользователю. TTFB < 50 мс для большинства регионов.
Деплой на Vercel / собственный сервер
Если нужен собственный хостинг:
// vite.config.ts — переключение на Node.js adapter
import { defineConfig } from 'vite';
import hydrogen from '@shopify/hydrogen/vite';
import { nodePreset } from '@shopify/remix-oxygen';
export default defineConfig({
plugins: [
hydrogen(),
nodePreset(), // вместо оксигенного
],
});
На Vercel — через @vercel/remix адаптер. На VPS — через node server.js с pm2.
Интеграция с CMS
Для управления контентом (баннеры, лендинги, блог) рядом с Hydrogen используется headless CMS:
- Contentful — GraphQL API, встроенный Asset CDN
- Sanity — GROQ-запросы, real-time обновления
- Prismic — Slice Machine для компонентной структуры
Запросы к CMS делаются в loader параллельно с запросами к Shopify:
export async function loader({ context }: LoaderFunctionArgs) {
const [{ product }, { hero }] = await Promise.all([
context.storefront.query(PRODUCT_QUERY, { variables }),
fetchFromCMS('home-hero'),
]);
return json({ product, hero });
}
Сроки
MVP headless-магазина на Hydrogen с деплоем на Oxygen (каталог, продукт, корзина, чекаут): 4–6 недель. Полноценный проект с CMS, мультирынком, кастомной аналитикой, A/B-тестингом и CI/CD: 2–4 месяца.







