Multi-page public site (Next.js), Node/Express backend with Postgres, and Vite admin/account app, restructured from a single-page layout into dedicated service, promotions, cases, about, FAQ, and blog pages for SEO. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
391 lines
27 KiB
Markdown
391 lines
27 KiB
Markdown
# TopSysOps - сайт + админ-панель
|
||
|
||
Продакшн-версия сайта top-sysops.ru: публичная часть с серверным рендерингом на
|
||
Next.js (Главная, Акции, Услуги, Кейсы, FAQ, Блог с рич-текст редактором, Отзывы,
|
||
Контакты), личный кабинет для клиентов (переписка с командой, ИИ-чат, профиль) и
|
||
админ-панель (дашборд посещаемости, модерация отзывов, редактор прайс-листа,
|
||
редактор акций, редактор кейсов и FAQ, редактор блога, переписка с клиентами,
|
||
ИИ-ассистент для сотрудников, виджет чата поддержки на сайте). SEO: sitemap.xml,
|
||
robots.txt и разметка schema.org генерируются самим Next.js.
|
||
|
||
## Из чего состоит
|
||
|
||
```
|
||
├── docker-compose.yml # четыре сервиса: db, backend, nextjs, web
|
||
├── .env.example # шаблон настроек, скопировать в .env
|
||
├── backend/ # Node.js/Express API + PostgreSQL
|
||
├── public-site/ # Next.js (SSR) - публичный сайт и блог
|
||
└── admin-app/ # React (Vite) - админка и личный кабинет + Caddy
|
||
```
|
||
|
||
- **db** - PostgreSQL. Хранит услуги/цены, акции, отзывы, статьи блога,
|
||
статистику визитов, клиентов и учётные записи администраторов.
|
||
- **backend** - REST API. При первом запуске сам накатывает миграции и создаёт
|
||
администратора и стартовый прайс-лист/акции (те же позиции, что были на старом сайте).
|
||
- **nextjs** - публичный сайт и блог, отдаются с сервера (SSR) - это и есть
|
||
ответ на "нужен SEO": краулер получает готовый HTML с разметкой schema.org,
|
||
а не пустую страницу, которую дорисовывает JavaScript. Не имеет отдельного
|
||
порта наружу - доступен только через Caddy.
|
||
- **web** - Caddy: собирает и раздаёт статику `admin-app` (админка и личный
|
||
кабинет - им SEO не нужен, поэтому остались обычным SPA), проксирует
|
||
`/api/*` на backend, всё остальное - на `nextjs`. Caddy же **сам получает и
|
||
продлевает HTTPS-сертификат** Let's Encrypt - вручную ничего настраивать не
|
||
нужно, только домен в `.env`.
|
||
|
||
Почему не переписали админку и кабинет на Next.js тоже: SEO им не нужно
|
||
вообще (они закрыты в `robots.txt` и требуют авторизации), а полный переезд
|
||
уже готовой и работающей админки ради нулевой выгоды - это только риск что-то
|
||
сломать. SSR применён именно там, где от него есть польза - на страницах,
|
||
которые должны индексироваться.
|
||
|
||
## Требования
|
||
|
||
- Сервер на Ubuntu 24.04 (2 ГБ RAM хватит с запасом).
|
||
- Домен, у которого A-запись указывает на IP сервера - нужен для автоматического HTTPS.
|
||
Если домена пока нет, можно временно развернуть по IP без HTTPS (см. `.env.example`).
|
||
- Открытые порты 80 и 443.
|
||
|
||
## 1. Установка Docker на Ubuntu 24
|
||
|
||
```bash
|
||
sudo apt update
|
||
sudo apt install -y ca-certificates curl gnupg
|
||
sudo install -m 0755 -d /etc/apt/keyrings
|
||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||
|
||
echo \
|
||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
|
||
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||
|
||
sudo apt update
|
||
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||
|
||
# чтобы не писать sudo перед каждой командой docker (перелогиньтесь после этого)
|
||
sudo usermod -aG docker $USER
|
||
```
|
||
|
||
Проверка: `docker compose version`.
|
||
|
||
## 2. Загрузка проекта на сервер
|
||
|
||
Скопируйте всю папку проекта на сервер, например через `scp` или `git`:
|
||
|
||
```bash
|
||
scp -r topsysops-app your-user@your-server-ip:~/topsysops-app
|
||
# или, если проект в git-репозитории:
|
||
git clone <ваш-репозиторий> topsysops-app
|
||
```
|
||
|
||
## 3. Настройка .env
|
||
|
||
```bash
|
||
cd topsysops-app
|
||
cp .env.example .env
|
||
nano .env
|
||
```
|
||
|
||
Обязательно замените:
|
||
- `POSTGRES_PASSWORD` - пароль базы данных;
|
||
- `JWT_SECRET` - длинная случайная строка (`openssl rand -hex 32`);
|
||
- `ADMIN_USERNAME` / `ADMIN_PASSWORD` - логин и пароль первого администратора;
|
||
- `DOMAIN` - ваш домен (например `top-sysops.ru`) или `:80`, если тестируете
|
||
по IP без HTTPS.
|
||
|
||
Файл `.env` содержит секреты - он уже добавлен в `.gitignore`, не публикуйте его.
|
||
|
||
## 4. Запуск
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Первый запуск соберёт образы (нужен интернет на сервере), поднимет базу,
|
||
применит миграции, создаст администратора и стартовый прайс-лист/акции, затем
|
||
запустит сайт. Проверить статус и логи:
|
||
|
||
```bash
|
||
docker compose ps
|
||
docker compose logs -f backend
|
||
```
|
||
|
||
Дождитесь строки `[start] backend listening on port 4000`, затем откройте
|
||
сайт в браузере по вашему домену (или `http://IP-сервера`, если `DOMAIN=:80`).
|
||
|
||
Если используете реальный домен, Caddy автоматически выпустит сертификат при
|
||
первом обращении по HTTPS - на это может уйти несколько секунд.
|
||
|
||
## Если TLS у вас на Nginx Proxy Manager
|
||
|
||
Именно этот сценарий - в `.env.example` он уже настроен по умолчанию:
|
||
`DOMAIN=http://top-sysops.ru`, `HTTP_PORT=7778`, порт 443 у контейнера
|
||
закомментирован в `docker-compose.yml`. Со стороны NPM:
|
||
|
||
1. **Hosts -> Proxy Hosts -> Add Proxy Host**.
|
||
2. **Domain Names**: ваш домен (`top-sysops.ru`).
|
||
3. **Scheme**: `http`, **Forward Hostname/IP**: IP или hostname сервера, где
|
||
запущен docker compose (`127.0.0.1`, если NPM на той же машине; иначе
|
||
реальный IP), **Forward Port**: `7778` (или то, что указали в `HTTP_PORT`).
|
||
4. Вкладка **SSL**: выберите/запросите сертификат Let's Encrypt, включите
|
||
**Force SSL** - весь HTTPS полностью на стороне NPM, контейнеру об этом
|
||
вообще думать не нужно.
|
||
5. Save. Проверьте, что `docker compose ps` показывает `web` со проброшенным
|
||
портом `7778->80` - именно на него должен смотреть Forward Port в NPM.
|
||
|
||
Если после этого видите "too many redirects" - смотрите раздел
|
||
"Устранение неполадок" ниже, обычно это означает, что `DOMAIN` в `.env`
|
||
задан без схемы `http://`.
|
||
|
||
## 5. Первый вход в админку
|
||
|
||
Откройте `https://ваш-домен/admin`, войдите под `ADMIN_USERNAME` / `ADMIN_PASSWORD`
|
||
из `.env`, затем сразу зайдите в **Настройки -> Сменить пароль** и задайте
|
||
собственный пароль - значение из `.env` дальше не используется системой, но
|
||
лучше не оставлять его действующим.
|
||
|
||
Из админки доступно:
|
||
- **Дашборд** - визиты (всего/сегодня/график за 7 дней), количество отзывов;
|
||
- **Отзывы** - одобрить / отклонить / удалить;
|
||
- **Клиенты** - список зарегистрированных клиентов и переписка с ними (ответ на
|
||
сообщения из личного кабинета, метка «ждёт ответа» для непрочитанных);
|
||
- **Прайс-лист** - добавление и редактирование категорий и позиций услуг,
|
||
скрытие позиции с сайта без удаления (иконка глаза);
|
||
- **Акции** - то же самое для акций;
|
||
- **Кейсы** - карточки портфолио (название, тег, статус с цветным индикатором,
|
||
описание), с возможностью скрыть без удаления;
|
||
- **FAQ** - вопросы и ответы для сайта;
|
||
- **Блог** - статьи (заголовок, слаг, краткое описание, текст в WYSIWYG-редакторе
|
||
с форматированием - заголовки, списки, ссылки, цитаты - публикация) -
|
||
попадают на по-настоящему серверно отрендеренную страницу `/blog/<slug>` и в
|
||
sitemap.xml сразу после сохранения;
|
||
- **Контакты** - адрес, телефон, email, режим работы, ссылка на карту;
|
||
- **ИИ-ассистент** - внутренний чат для сотрудников на базе OpenRouter (см. ниже);
|
||
- **Настройки** - смена пароля и управление администраторами (добавить нового
|
||
сотрудника с логином/паролем, удалить лишнего - кроме себя и последнего
|
||
оставшегося администратора).
|
||
|
||
Изменения в прайсе, акциях, кейсах и FAQ появляются на сайте в течение минуты:
|
||
Next.js кэширует данные с публичного сайта на 60 секунд (`revalidate: 60`), это
|
||
плата за серверный рендеринг - страницы отдаются мгновенно, а не после похода в
|
||
базу на каждый заход. В самой админке изменения видны сразу же, без задержки.
|
||
|
||
## Обновление прайс-листа на уже развёрнутом сайте
|
||
|
||
Стартовый прайс-лист (в `seed.js`) применяется только к пустой базе - то есть
|
||
только при самом первом запуске. Для уже развёрнутого сайта обновление новой
|
||
линейки услуг оформлено как обычные миграции
|
||
(`backend/migrations/006_update_services_2026_07.sql` и
|
||
`007_landing_tier.sql`) - они применяются **автоматически** при следующем
|
||
запуске бэкенда, ничего вручную запускать не нужно:
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Система миграций (`backend/src/migrate.js`) гарантирует, что каждый файл
|
||
выполнится ровно один раз - если уже разворачивали сайт раньше и обновляете
|
||
код сейчас, эти миграции просто окажутся среди тех, что применятся при
|
||
следующем перезапуске `backend`, наравне с остальными.
|
||
|
||
Что делают эти миграции: убирают старую категорию "Разработка сайтов"
|
||
(от 5 000 ₽) и добавляют:
|
||
- **Разработка под ключ** - Лендинг (от 20 000 ₽, простая посадочная без
|
||
бота/админки), MVP «Быстрый старт» (от 120 000 ₽, с формой-ботом и простой
|
||
админкой), MVP Pro (от 450 000 ₽, с личным кабинетом и ИИ-ассистентом - как
|
||
этот сайт), продакшн-разработка (от 1 200 000 ₽, индивидуально),
|
||
Discovery-воркшоп (от 30 000 ₽), сопровождение проекта (от 40 000 ₽/мес);
|
||
- **White-label и партнёрство** - white-label «Кабинет + ИИ-ассистент»
|
||
(от 500 000 ₽), IT-мониторинг сайта (от 10 000 ₽/мес), реферальная и
|
||
партнёрская программы.
|
||
|
||
Цифры сверены с рынком Санкт-Петербурга (лендинги студийного уровня -
|
||
19 000-65 000 ₽, MVP с личным кабинетом у студий - от 260 000 ₽ и выше).
|
||
|
||
Дальше все эти позиции редактируются как обычно - через админку («Прайс-лист»).
|
||
|
||
Если после `docker compose up -d --build` старая категория всё равно видна -
|
||
проверьте `docker compose logs backend | grep migrate`: там должны быть строки
|
||
`applying 006_update_services_2026_07.sql` и `applying 007_landing_tier.sql`.
|
||
Если их нет - значит контейнер `backend` не пересобрался/не перезапустился с
|
||
новым кодом (проверьте `docker compose ps`, при необходимости
|
||
`docker compose up -d --build backend` отдельно).
|
||
|
||
## SEO
|
||
|
||
- **robots.txt** и **sitemap.xml** - генерируются самим Next.js (`public-site/app/robots.js`
|
||
и `app/sitemap.js`), закрывают `/admin` и `/account` от индексации, доступны
|
||
по адресам `https://ваш-домен/robots.txt` и `/sitemap.xml`. Sitemap включает
|
||
главную, `/blog` и все опубликованные статьи - обновляется автоматически.
|
||
- **schema.org (JSON-LD)** - рендерится прямо на сервере (это заслуга SSR: разметка
|
||
оказывается в HTML, который получает краулер, а не дорисовывается JS уже в
|
||
браузере). На сайте автоматически публикуется `LocalBusiness` (адрес, телефон,
|
||
email из раздела «Контакты», рейтинг из одобренных отзывов), `FAQPage` (из
|
||
раздела FAQ) и каталог услуг (`hasOfferCatalog`, из прайс-листа) - помогает
|
||
Google/Яндексу показывать расширенные сниппеты (рейтинг со звёздами,
|
||
раскрывающиеся вопросы в выдаче). На страницах статей блога - разметка `Article`.
|
||
- **Блог с рич-текст редактором** - основной канал органического трафика для
|
||
B2B IT-услуг. Пишите статьи через админку («Блог», обычный WYSIWYG-редактор:
|
||
заголовки, списки, ссылки, цитаты) - каждая становится отдельной,
|
||
по-настоящему серверно отрендеренной страницей на `/blog/<slug>` и сразу
|
||
появляется в sitemap.xml.
|
||
- **Серверный рендеринг (SSR)** - публичный сайт и блог теперь на Next.js:
|
||
краулер и превью в соцсетях получают готовый HTML сразу, без ожидания
|
||
JavaScript. Админка и личный кабинет остались SPA на Vite - им это не нужно,
|
||
они и так закрыты в `robots.txt` и требуют входа.
|
||
- Что дальше сделать руками (не автоматизируется из кода): зарегистрировать сайт
|
||
в Яндекс.Вебмастере и Google Search Console (там же отправить sitemap.xml
|
||
вручную первый раз), завести профиль в Яндекс.Картах/Google Business с тем же
|
||
адресом и телефоном, что в разделе «Контакты» (важно для локального SEO по
|
||
запросам вида «IT-аутсорсинг Санкт-Петербург»).
|
||
|
||
## Чат поддержки на сайте (TopTicket / Chatwoot)
|
||
|
||
Виджет чата для посетителей сайта настраивается в `.env` и встраивается на
|
||
этапе сборки Next.js-приложения (значения переменных `NEXT_PUBLIC_*` попадают
|
||
в клиентский бандл, поэтому смена провайдера требует пересборки контейнера
|
||
`nextjs`, а не `web`).
|
||
|
||
По умолчанию уже включён TopTicket (ваша собственная разработка, help.top-sysops.ru)
|
||
с реальными данными. Выберите вариант в `.env`:
|
||
|
||
```bash
|
||
# TopTicket (по умолчанию) - embed-сниппет вашей разработки:
|
||
# <script src="https://help.top-sysops.ru/widget.js" data-key="..." async>
|
||
CHAT_PROVIDER=topticket
|
||
TOPTICKET_WIDGET_URL=https://help.top-sysops.ru/widget.js
|
||
TOPTICKET_WIDGET_KEY=EBpDS4JqT7a-_MzZ
|
||
|
||
# либо Chatwoot
|
||
CHAT_PROVIDER=chatwoot
|
||
CHATWOOT_BASE_URL=https://chat.lamlaba.ru
|
||
CHATWOOT_WEBSITE_TOKEN=ваш-website-token # Inboxes -> ваш инбокс -> Configuration
|
||
|
||
# либо совсем без чата
|
||
CHAT_PROVIDER=none
|
||
```
|
||
|
||
После изменения `.env`:
|
||
|
||
```bash
|
||
docker compose up -d --build nextjs
|
||
```
|
||
|
||
Виджет подключается только на публичных страницах сайта, в админке не показывается.
|
||
|
||
## ИИ-ассистент в админке (OpenRouter)
|
||
|
||
Это отдельный, второй чат - не для посетителей сайта, а внутренний инструмент
|
||
для сотрудников: помогает набросать ответ на отзыв, посчитать что-то по
|
||
прайсу и т.д. Доступен только тем, кто вошёл в админку.
|
||
|
||
Работает через [OpenRouter](https://openrouter.ai/) - агрегатор моделей разных
|
||
провайдеров под одним API. Ключ используется только на бэкенде и никогда не
|
||
попадает в браузер:
|
||
|
||
```bash
|
||
OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys
|
||
OPENROUTER_MODEL=openai/gpt-4o-mini # любая модель, доступная в вашем аккаунте
|
||
```
|
||
|
||
После заполнения:
|
||
|
||
```bash
|
||
docker compose up -d backend
|
||
```
|
||
|
||
Если `OPENROUTER_API_KEY` не задан, раздел «ИИ-ассистент» в админке просто
|
||
покажет предупреждение и не будет пытаться обращаться к API - остальной сайт
|
||
при этом продолжает работать как обычно.
|
||
|
||
## Контакты
|
||
|
||
Адрес, телефон, email, режим работы и ссылка на карту редактируются в админке
|
||
(раздел «Контакты», `/admin/contact`) — как и прайс-лист, акции, кейсы и FAQ,
|
||
без пересборки. Пустое поле просто не показывается на сайте (например, если
|
||
не хотите публиковать телефон).
|
||
|
||
## Личный кабинет клиентов
|
||
|
||
Отдельная система входа для посетителей сайта - не путать с админкой. Регистрация
|
||
и вход доступны по адресам `/account/register` и `/account`, на главной странице
|
||
есть заметная плашка-приглашение с этими же ссылками.
|
||
|
||
После входа клиенту доступны:
|
||
- **Обзор** - приветствие и текущие акции;
|
||
- **Написать нам** - личная переписка с командой TopSysOps. Сообщения клиента
|
||
видны в админке (раздел «Клиенты»), сотрудник отвечает прямо оттуда, ответ
|
||
сразу появляется в кабинете клиента;
|
||
- **ИИ-ассистент** - второй, отдельный чат, отвечает на вопросы об услугах и
|
||
ценах через тот же OpenRouter, что и ассистент в админке (см. выше), но с
|
||
собственным системным промптом и более строгим лимитом запросов;
|
||
- **Профиль** - изменение имени/компании/телефона, смена пароля.
|
||
|
||
Регистрация клиентов и вход в админку - две независимые системы: у них разные
|
||
таблицы в базе (`customers` и `admin_users`) и разные сессионные cookie
|
||
(`ts_customer_session` и `ts_admin_session`), так что сотрудник может быть
|
||
одновременно залогинен и в кабинет, и в админку в одном браузере без конфликтов.
|
||
|
||
Дополнительная защита от спама/перебора: на регистрацию, вход, отправку
|
||
сообщений и обращения к ИИ-ассистенту в кабинете клиента тоже действует
|
||
`express-rate-limit`, как и в остальном API.
|
||
|
||
## Резервное копирование базы данных
|
||
|
||
```bash
|
||
docker compose exec db pg_dump -U topsysops topsysops > backup-$(date +%F).sql
|
||
```
|
||
|
||
Восстановление из бэкапа:
|
||
|
||
```bash
|
||
cat backup-2026-07-25.sql | docker compose exec -T db psql -U topsysops topsysops
|
||
```
|
||
|
||
Рекомендуется добавить это в cron, например раз в сутки.
|
||
|
||
## Обновление после изменений в коде
|
||
|
||
```bash
|
||
git pull # если используете git
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Данные в PostgreSQL хранятся в именованном томе `db_data` и не теряются при
|
||
пересборке контейнеров.
|
||
|
||
## Устранение неполадок
|
||
|
||
- `docker compose logs -f backend` - если сайт не открывается, чаще всего
|
||
проблема видна тут (например, не заполнен `JWT_SECRET`).
|
||
- `docker compose logs -f nextjs` - если не открывается главная страница или
|
||
блог (например, не может достучаться до бэкенда - тогда секции будут просто
|
||
пустыми, а в логе будет `[serverApi] failed to fetch ...`).
|
||
- `docker compose logs -f web` - если не выдаётся HTTPS-сертификат, здесь будут
|
||
сообщения Caddy (обычно это неверный DNS: домен ещё не указывает на сервер).
|
||
- Если меняли `DOMAIN` в `.env`, перезапустите `web`: `docker compose up -d web`.
|
||
- **"Too many redirects" / бесконечный редирект** - значит перед контейнером
|
||
стоит внешний реверс-прокси со своим TLS-сертификатом (например, Nginx Proxy
|
||
Manager - см. раздел выше), а Caddy внутри контейнера всё равно пытается сам
|
||
сделать редирект http->https. Исправляется
|
||
так: в `.env` поставьте `DOMAIN=http://ваш-домен` (со схемой `http://`) и
|
||
перезапустите `web` (`docker compose up -d web`) - это отключит
|
||
автоматический HTTPS/редирект в Caddy, он будет просто отдавать http, а
|
||
шифрование останется на внешнем nginx. Также проверьте, что внешний nginx
|
||
передаёт заголовки `Host`, `X-Forwarded-Proto` и `X-Real-IP`/`X-Forwarded-For`.
|
||
- Полный сброс базы (например, во время тестов): `docker compose down -v`
|
||
удалит и данные - используйте только осознанно.
|
||
|
||
## Безопасность на будущее
|
||
|
||
- Не открывайте порт 5432 (PostgreSQL) наружу - в текущей конфигурации он и так
|
||
не проброшен на хост, доступен только другим контейнерам.
|
||
- Ограничьте доступ по SSH и настройте `ufw` (`ufw allow 80,443,OpenSSH`, затем
|
||
`ufw enable`). Если используете сценарий с внешним nginx и внутренним портом
|
||
вроде 7778 (`HTTP_PORT=7778`), этот порт наружу открывать не нужно - трафик
|
||
между nginx и контейнером идёт локально, через него наружу открыты только
|
||
80/443 самого nginx.
|
||
- `express-rate-limit` уже ограничивает количество попыток входа в админку и
|
||
количество отзывов с одного IP - это защита от перебора паролей и спама, но
|
||
не полноценный WAF; при росте нагрузки рассмотрите Cloudflare или аналог.
|