Initial commit: TopSysOps site + admin panel
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
This commit is contained in:
commit
130c5382cd
108 files changed
+7146
No files matched your search
@@ -0,0 +1,390 @@
|
||||
# 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 или аналог.
|
||||
Reference in new issue
Block a user