Перейти к содержимому

Витрина на Next.js

Витрина — сайт магазина, который видит покупатель. Это отдельное Next.js-приложение (App Router) со своим доменом и сервером. Данные оно берёт из Shop API через @fluttrium/shop-sdk. Здесь разобрано устройство витрины MOSHNA: так же удобно строить и новые.

Что Версия или значение
Next.js 16, App Router, output: "standalone"
React 19
@fluttrium/shop-sdk версия платформы на сервере
Типы запросов GraphQL Codegen со схемой из SDK

Переменные окружения витрины MOSHNA:

Переменная Что задаёт
VENDURE_SHOP_API_URL Адрес Shop API: https://api.fluttrium.store/shop-api. Из него же берётся хост картинок для next/image
VENDURE_CHANNEL_TOKEN Токен магазина
NEXT_PUBLIC_SITE_URL Адрес сайта: ссылки возврата из банка, канонические адреса, sitemap
NEXT_PUBLIC_CDEK_SERVICE_URL Сервис виджета СДЭК: https://api.fluttrium.store/cdek/widget?channel=<токен>
PAYMENT_METHOD_CODE Код способа оплаты на чекауте, по умолчанию yookassa
NEXT_PUBLIC_YANDEX_MAPS_API_KEY Ключ Яндекс Карт для виджета СДЭК
NEXT_PUBLIC_YANDEX_METRICA_ID Счётчик Метрики
  • Директорияapps/web/src
    • Директорияapp
      • (main) страницы: главная, catalog, product, cart, checkout, payment-success, profile, signin
      • api/health/route.ts проверка живости: витрина отвечает, только если Shop API видит её магазин
    • Директорияlib
      • store-config.ts устройство каталога и ключи контента этого магазина
      • Директорияvendure
        • shop.ts клиент Shop API: публичный с кешем и в сессии покупателя
        • actions.ts серверные действия: корзина, вход, оформление, оплата
        • safe-action.ts вызов действий с клиента
        • catalog.ts навигация, поиск, карточки товаров
        • content.ts контент сайта из админки
        • operations.ts свои запросы витрины
        • Директорияgql/ сгенерированные типы
          • …
    • components/providers/theme-style.tsx тема из админки в CSS-переменные
  • codegen.ts

В lib/vendure/shop.ts два входа в Shop API.

Функция Сессия Кеш Где вызывать
shopPublic(document, variables, { revalidate, tags }) нет Next.js data cache, по умолчанию 60 с Серверные компоненты: каталог, товары, контент
shop(document, variables) да, из cookie нет (no-store) Только серверные действия и обработчики маршрутов

Сессия покупателя хранится в httpOnly-cookie, браузер её не видит. Новый токен сессии, пришедший в ответе, shop() сразу записывает в cookie. Писать cookie Next.js разрешает только в серверных действиях и обработчиках маршрутов, поэтому shop() из серверных компонентов не вызывается.

Что Сколько Где задано
HTML страниц (ISR) 60 с export const revalidate = 60 в корневом layout.tsx и главной
Контент сайта 60 с, тег content lib/vendure/content.ts
Разделы каталога и аспекты 300 с, тег catalog lib/vendure/catalog.ts
Выдача каталога 60 с для запросов без поиска, с одним значением фильтра, на первых 5 страницах; остальное без кеша searchCatalog
Адреса товаров для отсечения несуществующих 300 с productSlugs

Поисковые фразы и сочетания фильтров в кеш не пишутся: каждое сочетание — новая запись на диске, перебором ими можно забить том.

Всё, что меняет корзину или аккаунт, — серверные действия в lib/vendure/actions.ts. Каждое возвращает результат, а не бросает ошибку:

type ActionResult<T = undefined> = { ok: true; data: T } | { ok: false; error: string };

Внутри действие разбирает ответ Shop API и переводит коды ошибок в понятный покупателю текст: INSUFFICIENT_STOCK_ERROR → «На складе осталось N шт.», ORDER_MODIFICATION_ERROR → «Заказ уже оформляется…». Сбой связи — «Не удалось связаться с магазином».

Правка строки корзины, которой уже нет в заказе (корзину открыли в двух вкладках или вернулись со страницы банка кнопкой «назад»), приходит от Shop API как USER_INPUT_ERROR. Действие отвечает «Корзина изменилась — показываем актуальную», и клиент перечитывает корзину.

Клиент вызывает действия через safely() из safe-action.ts. Он ловит то, что действие само не обработает: обрыв сети, 500 и обновление сайта.

Корзина на экране перечитывается, когда покупатель возвращается на вкладку (visibilitychange) или страница достаётся из кеша браузера кнопкой «назад» (pageshow), не чаще раза в 5 секунд.

placeOrder() выполняет шаги строго по порядку:

  1. Находит способ оплаты PAYMENT_METHOD_CODE среди доступных.
  2. Записывает покупателя (гостю) и адрес с именем и телефоном.
  3. Заново считает доставку СДЭК со свежей ценой — сервер платформы требует свежую котировку перед оплатой.
  4. Сверяет итог с суммой, которую видел покупатель. Разошлась — показывает новую сумму и просит нажать «Оплатить» ещё раз.
  5. Переводит заказ в ArrangingPayment и создаёт платёж с адресом возврата /payment-success?code=<код заказа>.
  6. Отправляет покупателя на страницу банка.

Банк не создал платёж — заказ возвращается в корзину. Что происходит после возврата из банка — на странице Платежи.

Возможности админки общие для всех магазинов. Как именно сайт раскладывает каталог и контент, решает витрина — в lib/store-config.ts.

Константа Что это У MOSHNA
FILTER_FACETS Аспекты, которые показываются фильтрами каталога, по порядку color, print, strap, closure, size
CATEGORY_FACET Аспект раздела в характеристиках товара category
SELECTION_COLLECTIONS Коллекции-подборки: отдельные ссылки в меню, а не разделы new, featured
BRAND Бренд в карточке товара Moshna
CONTENT.placements Места показа баннеров home.hero, home.gallery, home.shelves
CONTENT.menus Ключи меню header, header.info, footer, search.popular
CONTENT.texts Ключи текстов wholesale.tiers

Тема из админки (Маркетинг → Контент витрины → Оформление) попадает в CSS-переменные в theme-style.tsx. Витрина принимает только известные ключи и проверяет значения сама:

Ключ темы CSS-переменная Значение
color-accent --shop-accent #rgb или #rrggbb
font --font-shop inter-tight, helvetica, inter, manrope, geist, playfair, lora
fw-heading, fw-body, fw-button, fw-label --fw-* число 100–900
fs-carousel-title, fs-page-title, fs-product-title, fs-quiz-title --fs-* число 10–120, в пикселях

Запросы под свою вёрстку витрина пишет в operations.ts, типы генерирует от схемы из SDK — сеть для сборки не нужна:

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
import { shopClientPresetConfig, shopSchema } from '@fluttrium/shop-sdk/codegen';
const config: CodegenConfig = {
schema: shopSchema,
documents: ['src/**/*.{ts,tsx}', '!src/lib/vendure/gql/**'],
ignoreNoDocuments: true,
generates: {
'src/lib/vendure/gql/': {
preset: 'client',
presetConfig: { fragmentMasking: false },
config: shopClientPresetConfig,
},
},
};
export default config;

Платформа обновилась — поднимите версию @fluttrium/shop-sdk, запустите codegen и tsc: изменения схемы всплывут ошибками типов.

Фото и видео отдаёт сервер платформы по адресу https://api.fluttrium.store/assets/…. Витрина MOSHNA пропускает их через next/image только в WebP: AVIF кодируется в разы дольше, и первая выдача новой картинки после правки в админке ждала бы секунды. Точку фокуса (focalPoint) из админки витрина использует при кадрировании плиток.