899c54e1fc
Auth: drop localStorage session token entirely, talk to netrunner-backend via credentials:"include" HttpOnly cookies shared over the common parent domain, and redirect to the account subdomain (ЛК) after login instead of a dead local /profile route. Config: NEXT_PUBLIC_* values used by client components (API_URL, ACCOUNT_ORIGIN, BOT_USERNAME, PB_URL) get inlined into the JS bundle at image build time and can't be overridden by docker-compose environment at container start. Moved these reads into server components (page.tsx) and thread them down as props, so one built image can genuinely serve both prod and dev by config alone. CI/CD: split the old single auto-deploy-to-prod workflow into build.yml (build+push on every push to main) and deploy.yml with deploy-dev (auto, same VPS as netrunner-backend's dev environment) and deploy-prod (manual dispatch only) — mirrors netrunner-backend's pipeline shape. Added docker-compose.dev-remote.yml pointing at the dev backend. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
65 lines
6.5 KiB
Markdown
65 lines
6.5 KiB
Markdown
# `app/` — маршруты Next.js App Router
|
||
|
||
Весь пользовательский контент живёт под динамическим сегментом `app/[lang]/`,
|
||
кроме нескольких файлов верхнего уровня, которые Next.js требует держать вне
|
||
локализованных сегментов (`not-found.tsx`, `loading.tsx`, `globals.css`, `favicon.ico`).
|
||
|
||
## Как работает i18n через `[lang]`
|
||
|
||
1. Корневой `proxy.ts` (Next.js middleware) перехватывает каждый запрос. Если путь
|
||
уже начинается с `/en` или `/ru` — пропускает его как есть. Если локали в пути
|
||
нет (например, `/`) — определяет язык браузера через заголовок `Accept-Language`
|
||
(`Negotiator` + `@formatjs/intl-localematcher`) и редиректит на `/en...` или `/ru...`.
|
||
2. Дальше уже сам Next.js резолвит запрос в `app/[lang]/...`, а `params.lang`
|
||
передаётся во все `page.tsx`/`layout.tsx` этого сегмента.
|
||
3. `generateStaticParams` в проекте не используется — страницы под `[lang]` не
|
||
пре-генерируются статически на билде для `en`/`ru`, а рендерятся динамически
|
||
(server-side) по каждому запросу, с точечным ISR-кэшированием там, где это нужно
|
||
(см. `revalidate` в запросах к PocketBase в `Faq.tsx`, `BlogPreview.tsx`, `blog/page.tsx`).
|
||
4. Строковый контент (заголовки, лейблы, кнопки) берётся не из PocketBase, а из
|
||
JSON-словарей `dictionaries/en.json` / `dictionaries/ru.json`, которые грузятся
|
||
через `getDictionary(lang)` из `lib/dictionaries.ts` (только на сервере, файл
|
||
помечен `import "server-only"`).
|
||
|
||
## Паттерн `page.tsx` + `*-client.tsx`
|
||
|
||
Почти каждый раздел состоит из пары файлов:
|
||
|
||
- **`page.tsx`** — серверный async-компонент. Получает `params.lang`, грузит словарь
|
||
(`getDictionary`) и/или данные из PocketBase (`lib/pb.ts`), передаёт их вниз как пропсы.
|
||
- **`*-client.tsx`** — клиентский компонент (`"use client"`) с интерактивностью:
|
||
анимации на `framer-motion`, локальное состояние, realtime-подписки PocketBase,
|
||
запросы к Rust-шлюзу `proxy-ws` и т.д.
|
||
|
||
Разделение оправдано тем, что серверная часть не тянет в клиентский бандл лишний JS
|
||
(словари, PocketBase SDK для первого запроса), а клиентская часть свободно использует
|
||
хуки и браузерные API.
|
||
|
||
Есть и раздел без клиентской пары — `blog` и `blog/[slug]`, а также `Faq.tsx`/
|
||
`BlogPreview.tsx` в `components/sections/`: там контент статичен после загрузки
|
||
(no client-only interactivity), поэтому они остаются серверными компонентами целиком.
|
||
|
||
## Разделы
|
||
|
||
| Маршрут | Файлы | Назначение |
|
||
|---|---|---|
|
||
| `/` | `page.tsx` | Главная: статично рендерит `Hero`+`Features`, а `Pricing`/`FAQ`/`TerminalGuestbook`/`BlogPreview` подгружает через `next/dynamic` (code-splitting тяжёлых интерактивных секций, см. комментарий в файле) |
|
||
| `/auth` | `page.tsx` + `auth-client.tsx` | Авторизация через Telegram Login Widget против бэкенда на `account.netrunner-vpn.com` (`/users/me`, `/auth/telegram`, `credentials: "include"`); сессия — только HttpOnly cookie с `Domain=.netrunner-vpn.com`, лендинг ничего не хранит сам (ни `localStorage`, ни токен в URL). После успеха — полный переход на `{ACCOUNT_ORIGIN}/profile` (ЛК физически в другом сервисе, не в этом репо) |
|
||
| `/blog` | `page.tsx` | Список опубликованных постов из PocketBase (`posts_{lang}`), ISR-кэш на час |
|
||
| `/blog/[slug]` | `page.tsx` | Отдельный пост блога, контент рендерится как HTML (`dangerouslySetInnerHTML`), без кэша (`cache: "no-store"`) |
|
||
| `/docs` | `page.tsx` + `docs-client.tsx` | "Хранилище" документов (`documents_{lang}`) с поиском и фильтром по категориям, читалка на весь экран |
|
||
| `/download` | `page.tsx` + `download-client.tsx` | Клиенты VPN по платформам из PocketBase (`downloads_{lang}`), ссылки на прямое скачивание/магазины |
|
||
| `/nodes` | `page.tsx` + `nodes-client.tsx` | Список VPN-серверов со статусом/пингом/загрузкой — **сейчас данные моковые**, реального API ещё нет |
|
||
| `/speedtest` | `page.tsx` | Обёртка вокруг `components/sections/SpeedtestClient.tsx` — измерение скорости через Rust-шлюз `proxy-ws` |
|
||
| `/terminal` | `page.tsx` + `terminal-client.tsx` | Полноэкранная версия гостевой книги с постраничной подгрузкой истории |
|
||
|
||
## Откуда берутся данные
|
||
|
||
- **Переводы UI** — `dictionaries/en.json` / `dictionaries/ru.json` через `lib/dictionaries.ts`.
|
||
- **Динамический контент** (посты, FAQ, тарифы, документы, платформы загрузки,
|
||
комментарии гостевой книги) — PocketBase через синглтон `lib/pb.ts`. Коллекции
|
||
обычно разделены по языку суффиксом (`posts_en`/`posts_ru`, `faqs_en`/`faqs_ru` и т.д.).
|
||
- **Гостевая книга и спидтест** отправляют/читают трафик не напрямую в PocketBase,
|
||
а через Rust-шлюз `proxy-ws` (см. `/api/proxy/*` в `next.config.ts`), т.к. там
|
||
происходит модерация сообщений и реальные замеры скорости.
|