Skip to content

Partizap Frontend Architecture Design

Date: 2026-02-03 Status: Approved Backend: PHP Slim 4, PHP Sessions, cursor-based pagination, SERIAL IDs Deploy: VPS (Hetzner/Selectel), Node.js (Nitro) рядом с PHP


1. Tech Stack и ключевые решения

Runtime: Nuxt 4 (v4.3+) + TypeScript strict mode

Рендеринг: Hybrid — SSR для публичных страниц (главная, каталог, карточка товара, профиль продавца), SPA для приватных (кабинет, добавление объявления, настройки). Конфигурация через routeRules в nuxt.config.ts.

UI: Nuxt UI v3 (Tailwind 4 + Reka UI). Единая дизайн-система через app.config.ts — цвета, радиусы, типография.

State management: Pinia для глобального состояния (auth, geo, favorites), composables для локального (фильтры, формы, UI-состояние).

API-клиент: Обёртка над встроенным $fetch / useFetch. Автоматическая подстановка CSRF-токена из cookie в заголовок X-CSRF-TOKEN. Проброс session cookie при SSR через useRequestHeaders(['cookie']). Типизированные endpoint-функции в entities/*/api/.

Формы: Nuxt UI <UForm> + Zod-схемы. Схемы валидации определяются в entities/ и переиспользуются на Nitro-сервере для валидации.

Изображения: @nuxt/image для отображения (lazy loading, srcset, WebP/JPEG fallback через <NuxtPicture>). Кастомный useImageUpload composable для загрузки.

i18n: @nuxtjs/i18n — русский как основной, структура готова к добавлению языков.

SEO: @nuxtjs/seo — Schema.org (Product, Offer, LocalBusiness), динамический sitemap, robots.txt, OG-images.

Деплой: Node.js процесс (Nitro) на VPS рядом с PHP-бэкендом. PM2 или systemd для управления процессом.


2. Структура проекта (FSD-адаптированный под Nuxt)

partizap-frontend/
├── nuxt.config.ts
├── app.config.ts              # Nuxt UI тема, цвета, брендинг
├── app.vue                    # Root layout
├── i18n/
│   └── locales/
│       └── ru.json

├── pages/                     # Nuxt auto-routing (тонкие — layout + виджеты)
│   ├── index.vue              # Главная
│   ├── catalog/
│   │   └── index.vue          # Каталог с фильтрами
│   ├── product/
│   │   └── [id].vue           # Карточка товара
│   ├── auth/
│   │   └── index.vue          # Вход / Регистрация
│   ├── seller/
│   │   └── [id].vue           # Профиль продавца
│   └── cabinet/               # SPA-зона (routeRules: ssr: false)
│       ├── index.vue          # Мои объявления
│       ├── products/
│       │   ├── new.vue        # Добавить объявление
│       │   └── [id]/
│       │       └── edit.vue   # Редактировать
│       ├── favorites.vue
│       └── settings.vue       # Настройки профиля

├── layouts/
│   ├── default.vue            # Header + Footer (публичные)
│   └── cabinet.vue            # Header + Sidebar (кабинет)

├── shared/                    # FSD: переиспользуемое без бизнес-логики
│   ├── api/                   # $fetch обёртка, CSRF, cursor pagination helpers
│   ├── ui/                    # Базовые компоненты поверх Nuxt UI (если нужны обёртки)
│   ├── lib/                   # Утилиты: formatPrice, formatDate, pluralize
│   └── config/                # Константы: статусы, категории, route names

├── entities/                  # FSD: бизнес-сущности (данные + отображение)
│   ├── product/
│   │   ├── api/               # getProduct, getProducts, searchProducts
│   │   ├── model/             # типы: Product, ProductStatus, ProductFilters
│   │   ├── ui/                # ProductCard, ProductGallery, ProductSpecs
│   │   └── index.ts           # Public API сущности
│   ├── user/
│   ├── category/
│   ├── car/                   # Make, Model, Generation
│   └── geo/                   # Region, City, District, Metro

├── features/                  # FSD: пользовательские действия
│   ├── auth/                  # login, register, logout, reset-password
│   ├── add-listing/           # Создание/редактирование объявления
│   ├── search/                # Полнотекстовый поиск + YMM-фильтр
│   ├── favorites/             # Добавить/удалить из избранного
│   ├── geo-select/            # Выбор города/региона
│   └── image-upload/          # Загрузка + сортировка фото

├── widgets/                   # FSD: композиция entities + features
│   ├── header/                # Шапка: лого, поиск, гео, auth-статус
│   ├── footer/
│   ├── catalog-filters/       # Sidebar фильтров каталога
│   ├── product-list/          # Список товаров + cursor-пагинация
│   └── ymm-filter/            # Year/Make/Model каскадный фильтр

├── stores/                    # Pinia глобальные сторы
│   ├── auth.ts
│   ├── geo.ts
│   └── favorites.ts

└── server/                    # Nitro server routes (при необходимости)
    └── api/                   # Proxy к PHP API или SSR-хелперы

Ключевые правила FSD:

  • shared — не знает о бизнесе
  • entities — знает только о shared, не импортирует features/widgets
  • features — импортирует entities + shared
  • widgets — импортирует всё вышестоящее
  • pages — тонкие, только композиция виджетов + definePageMeta

Авто-импорт в nuxt.config.ts: директории shared/ui, shared/lib, entities/*/ui, features/*/ui, widgets/*/ регистрируются через components.dirs и imports.dirs.


3. API-слой и взаимодействие с PHP-бэкендом

Базовый клиент (shared/api/) — обёртка над $fetch с тремя задачами:

  1. CSRF: Читает CSRF_TOKEN из cookie, ставит в заголовок X-CSRF-TOKEN на каждый мутирующий запрос (POST/PUT/PATCH/DELETE). На GET не отправляется.

  2. Session cookie при SSR: При серверном рендеринге браузерные cookies недоступны — Nitro-сервер должен прокинуть cookie из входящего запроса пользователя в исходящий запрос к PHP API через useRequestHeaders(['cookie']). Ответные Set-Cookie от PHP тоже пробрасываются обратно клиенту через useRequestEvent().

  3. Cursor pagination: Хелпер useCursorPagination — принимает endpoint и фильтры, возвращает реактивные items, hasMore, loadMore(), refresh(). Под капотом передаёт cursor параметр из предыдущего ответа.

Типизированные API-функции живут в entities/*/api/:

entities/product/api/
├── getProducts.ts      # GET /store/products — список с фильтрами
├── getProduct.ts       # GET /store/products/:id — детали
├── searchProducts.ts   # GET /store/products/search — FTS
├── createProduct.ts    # POST /vendor/products
├── updateProduct.ts    # PUT /vendor/products/:id
└── index.ts            # Re-export public API

Каждая функция — тонкая обёртка: принимает типизированные параметры, вызывает базовый клиент, возвращает типизированный ответ. Zod-схемы ответов опциональны на MVP (доверяем бэкенду), но структура готова к добавлению runtime-валидации.

Формат ответа от PHP API (из MVP design):

typescript
// Успех (список)
{ data: Product[], meta: { has_more: boolean, next_cursor: string | null } }

// Успех (один объект)
{ data: Product }

// Ошибка
{ error: { code: string, message: string, details?: Record<string, string[]> } }

Error handling: Базовый клиент перехватывает HTTP-ошибки: 401 → редирект на auth, 403 → уведомление, 422 → пробрасывает details в форму, 429 → уведомление о rate limit, 500 → общая ошибка.


4. Routing, Route Rules и навигация

Hybrid rendering через routeRules в nuxt.config.ts:

SSR (server-rendered):
  /                          → Главная — SEO, FCP
  /catalog/**                → Каталог — индексация фильтрованных страниц
  /product/**                → Карточка товара — rich snippets, OG
  /seller/**                 → Профиль продавца — индексация
  /auth                      → Авторизация — быстрая загрузка

SPA (client-only, ssr: false):
  /cabinet/**                → Весь личный кабинет — не нужен SEO, быстрее навигация

ISR/SWR (кандидаты на будущее):
  /store/categories          → Дерево категорий — редко меняется, можно кэшировать
  /store/cars/makes          → Марки авто — статичные данные

Навигация и guards:

Middleware разделены на два уровня:

  • auth.global.ts — глобальный, на каждый переход: проверяет наличие сессии, подгружает authStore.user если не загружен (один запрос GET /auth/me при первом визите). Не блокирует публичные страницы.
  • cabinet.ts — named middleware на pages/cabinet/**: редирект на /auth если не авторизован, проверка email_verified_at для создания объявлений.

URL-структура (SERIAL IDs, чистые URL):

/                              Главная
/catalog                       Каталог (все)
/catalog?make=1&model=5        Каталог с фильтрами (query params)
/product/123                   Карточка товара
/seller/45                     Профиль продавца
/auth                          Вход / Регистрация (табы)
/auth?redirect=/cabinet/products/new   Редирект после входа
/cabinet                       Мои объявления
/cabinet/products/new          Добавить объявление
/cabinet/products/123/edit     Редактировать
/cabinet/favorites             Избранное
/cabinet/settings              Настройки профиля

Фильтры каталога хранятся в query params (не в store) — это даёт shareable URLs, работу кнопки «назад», SEO для фильтрованных страниц. Composable useCatalogFilters синхронизирует query params <-> реактивное состояние фильтров <-> API-запросы.


5. Pinia сторы и composables — разделение ответственности

Три глобальных стора:

authStore — сессия пользователя:

  • State: user (id, email, display_name, account_type, is_admin, verified flags), isAuthenticated
  • Actions: fetchMe() (GET /auth/me), login(), register(), logout(), logoutAll()
  • Инициализация: вызывается из auth.global.ts middleware при первом визите. При SSR — запрос к PHP API с проброшенными cookies. Результат гидратируется на клиент, повторный запрос не нужен.

geoStore — выбранный город/регион:

  • State: currentCity, currentRegion, searchRadius
  • Actions: setCity(), detectByIP(), loadRegions(), loadCities(regionId)
  • Персистенция: cookie geo_city_id (доступна и на сервере при SSR, и на клиенте). При первом визите: читаем cookie → если пусто, определяем по IP → если не удалось, «Все регионы». Для залогиненного пользователя: синхронизация с users.city_id из профиля (по ТЗ — при смене города в хедере спрашиваем «Сохранить в профиль?»).

favoritesStore — избранные товары:

  • State: favoriteIds (Set<number>), count
  • Actions: toggle(productId), loadFavorites(), isFavorite(productId)
  • Для авторизованных: синхронизация с сервером. Для гостей: localStorage (при регистрации — merge с сервером, deferred для post-MVP).

Composables (локальное состояние):

ComposableОбластьЧто делает
useCatalogFiltersКаталогСинхронизация query params <-> фильтры <-> API
useCursorPaginationКаталог, спискиitems, hasMore, loadMore(), refresh()
useYmmFilterГлавная, каталогКаскадный выбор Make → Model → Generation
useImageUploadДобавить объявлениеПревью, drag-and-drop, сортировка, загрузка
useProductFormДобавить/редактироватьСостояние формы, Zod-валидация, submit
usePhoneRevealКарточка товараЛенивая загрузка телефона продавца по клику

Правило границы: если состояние нужно на 2+ несвязанных страницах — Pinia store. Если привязано к одному экрану или компоненту — composable.


6. SEO, Schema.org и мета-данные

Стратегия по страницам:

Карточка товара (/product/:id) — главный приоритет SEO:

  • useSeoMeta: title = {title} — купить в {city} | Partizap, description из первых 160 символов описания, OG-image из primary фото
  • Schema.org: Product + Offer (price, availability, condition) + LocalBusiness (продавец). Это даёт rich snippets в Google: фото, цена, наличие
  • Canonical URL: /product/{id}

Каталог (/catalog):

  • Динамический title по активным фильтрам: «Запчасти BMW X5 E53 в СПб» → {категория} {марка} {модель} {поколение} в {город}
  • Schema.org: ItemList с позициями
  • Фильтры в query params индексируются (make + model комбинации), остальные — noindex через meta robots для предотвращения дублей

Главная (/):

  • Статичный title + description, Schema.org: WebSite + SearchAction (для sitelinks searchbox в Google)

Профиль продавца (/seller/:id):

  • Schema.org: LocalBusiness или Person в зависимости от account_type

Кабинет (/cabinet/**):

  • noindex, nofollow — приватные страницы не индексируются

Sitemap (@nuxtjs/sitemap):

  • Динамический: endpoint в server/api/__sitemap__/ запрашивает у PHP API список активных товаров и продавцов с updated_at для lastmod
  • Разбивка на sitemap index если >50K URL
  • Приоритет: карточки товаров > каталог по маркам > профили продавцов

Robots.txt:

User-agent: *
Allow: /
Disallow: /cabinet/
Disallow: /auth
Sitemap: https://partizap.ru/sitemap.xml

7. Обработка изображений и медиа

Отображение (@nuxt/image):

Провайдер — кастомный или ipx проксирующий к S3/CDN. Бэкенд генерирует 3 размера в двух форматах (из MVP design):

РазмерНазначениеГде используется
thumbnail (150x150)Превью в списках, сетка каталогаProductCard, избранное
medium (600x600)Превью в галерее карточкиProductGallery thumbnails
large (1200x1200)Полноразмерное фотоProductGallery основное фото

Компонент <NuxtPicture> отдаёт WebP с JPEG fallback:

html
<NuxtPicture
  :src="image.thumbnail_webp"
  :fallback="image.thumbnail_jpeg"
  loading="lazy"
  sizes="150px sm:200px"
/>

Загрузка (features/image-upload/):

Composable useImageUpload инкапсулирует полный флоу:

  1. Выбор файлов: input[type=file] + drag-and-drop зона. Ограничения: max 5 файлов, JPEG/PNG/WebP, до 10MB каждый. Валидация на клиенте до отправки.

  2. Превью: URL.createObjectURL() — мгновенное отображение без загрузки на сервер. Освобождение через URL.revokeObjectURL() при удалении или unmount.

  3. Загрузка: Каждый файл отправляется отдельным POST /vendor/products/:id/images с FormData. Прогресс через xhr.upload.onprogress (ofetch не поддерживает upload progress — для этого случая прямой XMLHttpRequest).

  4. Статус обработки: Бэкенд возвращает status: 'processing'. Клиент поллит GET /vendor/products/:id пока все изображения не получат status: 'ready'. Показываем спиннер на необработанных фото.

  5. Сортировка: Drag-and-drop переупорядочивание, сохранение через PUT /vendor/products/:id/images/order с массивом [imageId, ...].

  6. Удаление: DELETE /vendor/products/:id/images/:imgId с optimistic update в UI.


8. Зависимости

Core

ПакетВерсияНазначение
nuxt^3.15Фреймворк
vue^3.5(peer dependency)
typescript^5.7Strict mode

Nuxt Modules

МодульНазначение
@nuxt/uiUI-компоненты + Tailwind 4 + Reka UI
@nuxt/imageОптимизация изображений, <NuxtPicture>
@nuxtjs/i18nИнтернационализация
@nuxtjs/seoSchema.org, sitemap, robots, OG
@pinia/nuxtState management
pinia(peer dependency)

Библиотеки

ПакетНазначение
zodВалидация форм и API-ответов
@vueuse/coreУтилитарные composables (useStorage, useDebounceFn, useIntersectionObserver)

Dev Dependencies

ПакетНазначение
@nuxt/test-utilsТестирование Nuxt-компонентов
vitestTest runner
@vue/test-utilsUnit-тесты компонентов
eslintЛинтинг
@nuxt/eslintESLint конфиг для Nuxt
prettierФорматирование

Сознательно НЕ включены

ПакетПричина
axiosДублирует встроенный $fetch / ofetch
vee-validateNuxt UI Form + Zod покрывает потребности
pinia-plugin-persistedstateДля auth — cookies вручную, для geo — cookie через useCookie, для favorites — useLocalStorage из VueUse
@nuxtjs/color-modeВходит в Nuxt UI
tailwindcssВходит в Nuxt UI v3

Итого: 8 runtime-зависимостей, 6 dev-зависимостей.