- Страницы обоих брендов из блоков Payload (D-029): главная, цены, локации, поддержка, 4 юридические, 404 и 500. Коллекции pages (черновики и версии) и faq с местами показа, 12 типов блоков; поля sites — почта и Telegram поддержки, часы работы, SEO по умолчанию. - Карта и глобус рисуются на сервере (d3-geo), контуры во внешнем SVG через <use>; выбор локации синхронизирован между картой и сеткой. - Тексты сайта и юридические черновики на 5 языках: de, fr, es — перевод с глоссарием; seed создаёт страницы и FAQ, только если их нет, SEED_CONTENT=reset пересоздаёт. - SEO: title и описания, canonical, hreflang с x-default, sitemap.xml и robots.txt на каждый домен, картинки для соцсетей. - Статическая сборка и пересборка через revalidatePath из хуков Payload (D-030); /svc/internal/revalidate для проверки. - 404 — статичная страница со статусом 404 через proxy; на сайте только опубликованные страницы. - Производительность: Client Hints темы Payload только в админке, предзагрузка 2 файлов шрифтов вместо 6; Lighthouse mobile 97–99. - Контраст --ink-3 по WCAG AA, радиокнопки вместо div role=button. - Документация: итоги 1b, глоссарий de/fr/es, трактовки юридических черновиков подтверждены, переносимые вопросы — в этапах 3e–3h. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
14 KiB
Markdown
134 lines
14 KiB
Markdown
# 02. Архитектура
|
||
|
||
## 1. Стек
|
||
|
||
| Что | Версия на 20.10.2026 | Комментарий |
|
||
|---|---|---|
|
||
| Node.js | 24 LTS (на сервере 24.22) | |
|
||
| pnpm | 12.x | ставится root через `corepack enable pnpm`; версия фиксируется полем `packageManager` |
|
||
| Next.js | 16.4.x | App Router, React Server Components |
|
||
| React | 19.x | |
|
||
| Payload CMS | 3.91.x | Payload 4 в бете; переход после стабильного релиза ([D-002](decisions/D-002-payload-version.md)) |
|
||
| PostgreSQL | 18 (на хосте) | адаптер `@payloadcms/db-postgres`, `idType: 'uuid'` |
|
||
| Tailwind CSS | 4.x | токены из макета как CSS-переменные |
|
||
| TypeScript | 6.0 | не 7.0: typescript-eslint из eslint-config-next поддерживает только TS < 6.1 |
|
||
| ESLint | 9.39 | не 10: eslint-plugin-react, import и jsx-a11y не заявляют поддержку ESLint 10 |
|
||
| Vitest | 5.x | модульные тесты |
|
||
| next-intl | 4.x | переводы интерфейса, префиксы языков в URL |
|
||
| Nodemailer | через `@payloadcms/email-nodemailer` | транспорты SMTP, sendmail, «только журнал» |
|
||
| ALTCHA | 3.x | капча без третьих сторон, этап 3a |
|
||
| grammY | 1.x | Telegram-боты, этапы 2a–2c |
|
||
| Stripe SDK | актуальный | этап 4b |
|
||
|
||
Точные версии закреплены в `package.json`: на этапе 1a — Next 16.4.2, Payload 3.91.0, React 19.3.1, next-intl 4.14.9, Tailwind 4.3.3, pnpm 12.14. На старте 1a Payload 4 был в бете (4.0.0-beta.3), поэтому начали на 3.91 ([D-002](decisions/D-002-payload-version.md)). Телеметрия Next.js и Payload выключена.
|
||
|
||
## 2. Одно приложение на все домены
|
||
|
||
1. Одно Next.js-приложение, Payload установлен внутрь как библиотека: админка, API и фронтенды — один процесс ([D-001](decisions/D-001-single-app.md)).
|
||
2. Proxy Next.js (`src/proxy.ts`, бывший middleware) по заголовку Host находит домен и узнаёт сайт (`dedicated` или `mass`) и тип фронтенда: сайт, кабинет или MiniApp. Список активных доменов proxy берёт у `/svc/internal/domains` (по токену из `.env`) и держит в памяти 15 секунд: правка домена в админке применяется в пределах 15 секунд ([D-022](decisions/D-022-proxy-routing.md)).
|
||
3. Proxy переписывает запрос во внутренний маршрут по хосту: `/m/{хост}/{язык}/{валюта}/…` — сайт, `/c/{хост}/{язык}/…` — кабинет, `/t/{хост}/…` — MiniApp. Снаружи эти пути недоступны: прямой запрос к ним получает ещё один префикс и попадает в 404. Решения proxy — чистая функция `src/core/routing/decide.ts` с модульными тестами.
|
||
4. Хост админки (`plc.veilix.cc`, `dev.plc.veilix.cc`) задаётся в `.env`. Только на нём работают `/admin` и REST API Payload. На доменах брендов эти пути закрыты.
|
||
5. Сайтам, кабинетам и MiniApp хост админки не нужен:
|
||
a. данные Payload они читают в том же процессе через Local API — это вызов функции, а не HTTP-запрос;
|
||
b. запросы браузера (формы, вход, действия в кабинете) идут на тот домен, где открыта страница: server actions и эндпоинты `/svc/…`;
|
||
c. вебхуки платёжных систем и Telegram приходят на публичные домены, не на хост админки;
|
||
d. файлы медиатеки отдаются через домены сайтов.
|
||
6. Ядро общее (биллинг, ключи, вход, рефералы, почта), UI-кит общий, а страницы у каждого бренда свои: данные в дизайнах представлены по-разному.
|
||
|
||
## 3. Структура кода
|
||
|
||
```
|
||
src/
|
||
proxy.ts — хост → домен, язык, валюта, внутренний маршрут
|
||
payload.config.ts
|
||
payload-types.ts — типы Payload (pnpm generate:types)
|
||
app/
|
||
(payload)/ — админка и REST API Payload (стандартные файлы)
|
||
(front)/m/[host]/[locale]/[currency]/(site)/… — маркетинговые сайты; site-page.tsx — страница из блоков
|
||
(front)/c/[host]/[locale]/(shell)/… — кабинет с навигацией
|
||
(front)/c/[host]/[locale]/(plain)/… — вход и оформление заказа
|
||
(front)/t/[host]/… — MiniApp (этап 2a)
|
||
(front)/context.ts — домен, язык и валюта страницы, данные оболочек
|
||
svc/… — свои эндпоинты: health, internal/domains и internal/revalidate (по токену),
|
||
seo/{хост}/robots.txt и sitemap.xml, og/… (картинки для соцсетей),
|
||
geo/… (контуры карты); дальше ALTCHA, вебхуки
|
||
payload/
|
||
collections/ blocks/ access/ fields/ hooks/ components/
|
||
core/ — бизнес-логика без привязки к UI
|
||
sites.ts domains/ routing/ currencies/ payments/ — этап 1a
|
||
pages.ts content/ seo/ — этап 1b: страницы, данные сайта, sitemap
|
||
billing/ keys/ provisioning/ auth/ referral/ email/ geo/ — следующие этапы
|
||
brands/
|
||
dedicated/ mass/ — различия брендов; страницы и компоненты — с этапа 1b
|
||
ui/ — общие компоненты: оболочки сайта и кабинета, переключатели, тема, логотип, иконки;
|
||
marketing/blocks/ — блоки страниц сайта, marketing/geo.ts — карта и глобус
|
||
i18n/ — языки, конфиг next-intl и messages/{en,ru,de,fr,es}.json
|
||
styles/ — Tailwind, токены брендов
|
||
seed/ — начальные данные (pnpm seed): content/{язык}.json — тексты страниц и FAQ,
|
||
legal/{язык}/*.md — юридические черновики
|
||
ops/systemd/ — юниты systemd
|
||
scripts/ — служебные скрипты (очистка dev-базы)
|
||
```
|
||
|
||
Корневые layout сайта и кабинета дают только документ: `<html>` с `lang`, `data-site` и скриптом темы. Шапка, подвал и навигация — во вложенных группах `(site)`, `(shell)` и `(plain)`.
|
||
|
||
Бизнес-логика живёт в `core/` и вызывается одинаково из хуков Payload, server actions, вебхуков, фоновых задач и (с этапа 2b) бота.
|
||
|
||
## 4. Окружения
|
||
|
||
| | dev | prod |
|
||
|---|---|---|
|
||
| Пользователь ОС | vx_dev (под ним работают редактор и сессии Claude) | vx_prod (без редактора и ИИ-агентов) |
|
||
| Папка | `/home/vx_dev/veilix_dev` — рабочая копия git | `/home/vx_prod/veilix` — `releases/`, симлинк `current`, `shared/` |
|
||
| Запуск | `pnpm dev` (горячая перезагрузка) | `pnpm start` из `current` после `pnpm build` |
|
||
| Порт | 3100 | 3000 |
|
||
| База | `veilix_dev` | `veilix_prod` |
|
||
| Схема базы | синхронизируется автоматически (push); если push задаёт вопросы — `pnpm db:reset` и `pnpm seed` | миграции Payload, применяются при выкладке |
|
||
| Домены | `dev.*`, `dev.plc.veilix.cc` | основные, `plc.veilix.cc` |
|
||
| Секреты | `/home/vx_dev/veilix_dev/.env` | `/home/vx_prod/veilix/shared/.env` |
|
||
| Файлы (media) | `/home/vx_dev/veilix_dev/media` | `/home/vx_prod/veilix/shared/media` |
|
||
|
||
1. Без Docker: оба окружения — обычные процессы Node, каждый под своим пользователем ОС, запускаются одинаково — systemd user-юнитами: `veilix-dev.service` у vx_dev, `veilix.service` у vx_prod. У vx_dev нет доступа к файлам, секретам, логам и базе prod. Подробно — [D-003](decisions/D-003-hosting.md), [D-004](decisions/D-004-environments.md).
|
||
2. Dev закрыт от посторонних в nginx и запрещён к индексации, см. [09-infrastructure.md](09-infrastructure.md).
|
||
3. Платёжные системы в dev — только в тестовом режиме.
|
||
|
||
## 5. Выкладка в prod
|
||
|
||
1. В dev: изменения схемы оформляются миграцией (`payload migrate:create`), коммит, push в Gitea, тег релиза. До первой выкладки (этап 3h) миграции не накапливаем: перед ней создаём одну начальную по итоговой схеме. Механизм проверен на этапе 1a: миграция, применённая к пустой схеме, создаёт те же таблицы, что push в dev.
|
||
2. Выкладка — команда `deploy <тег>` пользователя vx_prod, появится на этапе 3h. Запускает её пользователь; по желанию — sudo-правило, разрешающее vx_dev запускать только эту команду (Q-18). Шаги:
|
||
a. взять тег из Gitea (у vx_prod ключ только на чтение) в новую папку `releases/<дата-тег>`;
|
||
b. `pnpm install --frozen-lockfile` и `pnpm build`; ссылки на `shared/.env` и `shared/media`;
|
||
c. применить миграции к `veilix_prod`;
|
||
d. переключить симлинк `current` на новый релиз и перезапустить `veilix.service`;
|
||
e. проверить `/svc/health`; хранить 5 последних релизов.
|
||
3. Откат: переключить `current` на прошлый релиз и перезапустить. Миграции пишем так, чтобы прошлый релиз продолжал работать с новой схемой (сначала добавляем, удаляем позже).
|
||
4. Простой при выкладке — только время перезапуска процесса, несколько секунд.
|
||
|
||
## 6. Рендеринг и кэширование
|
||
|
||
1. Маркетинговые страницы собираются статически в вариантах «домен × язык × валюта» при первом запросе (ISR) и кэшируются. Proxy выбирает вариант по префиксу языка и cookie валюты.
|
||
2. Изменение в админке (цена, статус локации, FAQ, текст страницы) вызывает хук Payload, и Next.js пересобирает маркетинговые страницы при следующем заходе (`revalidatePath`, [D-030](decisions/D-030-revalidate-path.md)). Запасной вариант — плановое обновление раз в 5 минут.
|
||
3. При оформлении заказа наличие и цена проверяются заново на сервере. Устаревшая страница не может продать то, чего нет.
|
||
4. Кабинет рендерится на сервере (SSR) для каждого запроса, без кэша.
|
||
5. Маркетинговые домены не ставят cookies, кроме выбора языка, валюты и темы.
|
||
|
||
## 7. Фоновые задачи
|
||
|
||
Встроенная очередь Payload (Jobs Queue) с расписанием cron. Исполнитель работает внутри процесса приложения (`autoRun`), в dev его можно отключить переменной окружения.
|
||
|
||
| Задача | Расписание | Что делает |
|
||
|---|---|---|
|
||
| `renewals` | каждые 15 минут | продление ключей, у которых срок подходит (см. [04-billing.md](04-billing.md)) |
|
||
| `reminders` | каждый час | напоминания за 3 дня до окончания |
|
||
| `expiry` | каждые 15 минут | перевод в льготный период и в «истёк» |
|
||
| `send-email` | по событию | отправка письма с повторами при ошибке |
|
||
| `cleanup` | раз в сутки | старые коды входа, сессии, записи журнала писем |
|
||
| `reconcile` | раз в сутки | сверка кэша баланса с суммой движений по счёту |
|
||
|
||
## 8. Чего не используем
|
||
|
||
1. Docker — не нужен ни в dev, ни в prod ([D-003](decisions/D-003-hosting.md)).
|
||
2. n8n — боты пишем в коде на grammY ([D-015](decisions/D-015-telegram-bots.md)).
|
||
3. listmonk — шаблоны писем в Payload, отправка своим транспортом ([D-014](decisions/D-014-email.md)).
|
||
4. Redis — стоит на сервере, но пока не нужен: лимиты запросов и очередь задач работают на PostgreSQL.
|