Витрина на 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() выполняет шаги строго по порядку:
- Находит способ оплаты
PAYMENT_METHOD_CODEсреди доступных. - Записывает покупателя (гостю) и адрес с именем и телефоном.
- Заново считает доставку СДЭК со свежей ценой — сервер платформы требует свежую котировку перед оплатой.
- Сверяет итог с суммой, которую видел покупатель. Разошлась — показывает новую сумму и просит нажать «Оплатить» ещё раз.
- Переводит заказ в
ArrangingPaymentи создаёт платёж с адресом возврата/payment-success?code=<код заказа>. - Отправляет покупателя на страницу банка.
Банк не создал платёж — заказ возвращается в корзину. Что происходит после возврата из банка — на странице Платежи.
Что задаёт дизайн магазина
Заголовок раздела «Что задаёт дизайн магазина»Возможности админки общие для всех магазинов. Как именно сайт раскладывает
каталог и контент, решает витрина — в 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 — сеть для сборки не нужна:
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) из админки витрина использует при кадрировании плиток.