Shumiland/docs/domain-migration.md
Vadym Samoilenko 03a0af4080
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
feat(routing): move /kvytky → /payments, rename checkout → /checkout
- /payments now serves the main ticket catalog (was /kvytky)
- /checkout serves the legacy single-tariff checkout form
- All /kvytky references updated across components, layout, lib, seed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 10:44:34 +01:00

204 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Runbook: миграция на боевой домен shumiland.com.ua
> **Триггер для новой сессии:** когда пользователь скажет «мигрируем на shumiland.com.ua» (или похожее) — прочитать этот файл и выполнять по шагам A → B → C → D. Реализация ещё НЕ начата; ждём подтверждение заказчика.
## Context
Сайт работает на staging `https://shumi.ai-impress.com/` (VPS `147.135.209.100`, Docker Compose: Postgres + Next.js standalone + Caddy). Caddy сейчас отдаёт **self-signed cert для голого IP** (`auto_https off`). Переключаем продакшн на `shumiland.com.ua`.
**Подтверждённые решения:**
- **DNS** — переезжаем с hostiq.ua на **Cloudflare** (Free). Свой авторитативный NS не поднимаем.
- **SSL** — гибрид: публичный домен через прокси Cloudflare (Origin Cert), админка-сабдомен напрямую (Let's Encrypt).
- **Старый домен** — полный переход, `shumi.ai-impress.com` больше не обслуживается.
- **Resend** — верифицируем домен сейчас, письма с `noreply@shumiland.com.ua`.
**Почему гибрид (важно):** в админку грузят видео **>100 MB**, а Cloudflare Free режет тело запроса на 100 MB (не поднять без Enterprise). Поэтому публичный трафик — через прокси, а загрузки/админка — в обход прокси через DNS-only сабдомен.
| Хост | Cloudflare | Сертификат | Назначение |
| ------------------------- | ------------------- | ------------------------------ | ------------------------------------- |
| `shumiland.com.ua`, `www` | Proxied (оранжевое) | CF Origin Cert + Universal SSL | Публичный сайт: CDN, DDoS, скрытый IP |
| `cms.shumiland.com.ua` | DNS only (серое) | Let's Encrypt (Caddy) | Админка + загрузки без лимита 100 MB |
**Ключевой нюанс `NEXT_PUBLIC_SITE_URL`:** используется в двух режимах:
- _Server-side (runtime)_ — `payload.config.ts` (serverURL, cors, livePreview), `src/middleware.ts`. Берётся из `.env.production` при запуске контейнера. **Главный рычаг.**
- _Client-side (build-time)_ — `src/components/cms/RefreshRouteOnSave.tsx` (live preview). Запекается при сборке образа. CI (`deploy.yml`) сейчас build-arg НЕ передаёт → значение пустое. Влияние минимальное, но чиним (B.3).
---
## Часть A — DNS: переезд с hostiq на Cloudflare
Домен делегирован на `dns1/dns2.hostiq.ua`. «Свої сервери імен» в nic.ua = поле для NS провайдера, НЕ свой DNS-сервер.
1. **Cloudflare → Add a site** `shumiland.com.ua` (план Free). Cloudflare выдаст **пару своих NS** (`xxx.ns.cloudflare.com` / `yyy.ns.cloudflare.com`).
2. **nic.ua → Сервери імен (NS) → «Список своїх серверів імен» → Змінити**: удалить `dns1/dns2.hostiq.ua`, вписать **две NS Cloudflare**. Дождаться статуса **Active** в Cloudflare.
3. **Записи в Cloudflare** (DNS → Records):
- `A @ → 147.135.209.100`**Proxied (оранжевое)**
- `A www → 147.135.209.100`**Proxied (оранжевое)**
- `A cms → 147.135.209.100`**DNS only (серое)** ← админка/загрузки в обход прокси
4. **SSL/TLS → Overview → mode = Full (strict)**.
5. **SSL/TLS → Origin Server → Create Certificate** (покрыть `shumiland.com.ua` + `*.shumiland.com.ua`). Сохранить cert + key → на VPS в `./certs/origin.pem` и `./certs/origin-key.pem` (шаг C.3).
6. **Rules → Cache Rules**: bypass cache для `/admin*` и `/api/*` (иначе админка/формы поедут). Статику CF кэширует — это плюс.
7. **Rules → Redirect Rules** (опционально): `www.shumiland.com.ua``shumiland.com.ua` (301).
8. Проверить: `dig +short shumiland.com.ua` → CF-адреса (проксировано); `dig +short cms.shumiland.com.ua``147.135.209.100`.
> ⚠️ `cms` держать **DNS only** — иначе (а) упрёмся в лимит 100 MB, (б) Let's Encrypt на Caddy не выпустит cert через прокси (HTTP-01 на порту 80). DNS `cms` должен резолвиться на VPS ДО рестарта Caddy (C.4).
---
## Часть B — Изменения в репозитории (commit + push в `main`)
### B.1. `Caddyfile` — публичный домен на Origin Cert, `cms` на Let's Encrypt
Заменить весь файл:
```caddyfile
{
email admin@shumiland.com.ua
}
# Публичный сайт — за прокси Cloudflare, Origin Certificate
shumiland.com.ua, www.shumiland.com.ua {
tls /certs/origin.pem /certs/origin-key.pem
reverse_proxy app:3000
}
# Админка/загрузки — DNS-only, обход лимита 100 MB, реальный Let's Encrypt cert
cms.shumiland.com.ua {
reverse_proxy app:3000
}
```
Глобальный `auto_https off` **не** ставим: блок с явным `tls` сам отключает авто-выпуск для своих имён, а `cms` без `tls` → Caddy выпустит Let's Encrypt.
### B.2. `docker-compose.prod.yml`
- `app.build.args.NEXT_PUBLIC_SITE_URL`: `https://147.135.209.100``https://shumiland.com.ua` (строка 39).
- `app`: удалить `environment.NODE_EXTRA_CA_CERTS` (строка 43) и mount `./certs:/certs:ro` (строка 51) — self-signed обвязка больше не нужна.
- Сервис `caddy`: **оставить** mount `./certs:/certs:ro` (там Origin Cert); добавить персистентные тома для Let's Encrypt-cert'а `cms`:
```yaml
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./certs:/certs:ro
- caddy_data:/data
- caddy_config:/config
```
- В блок `volumes:` добавить `caddy_data:` и `caddy_config:`.
### B.3. `.github/workflows/deploy.yml` — запекать корректный client-side URL
В шаг `Build and push` (после `cache-to`) добавить:
```yaml
build-args: |
NEXT_PUBLIC_SITE_URL=https://shumiland.com.ua
```
### B.4. `payload.config.ts` — добавить `cms`-хост в cors/csrf
Админка на `cms.shumiland.com.ua`, serverURL = публичный домен → разрешить cms-origin, иначе логин/запросы в админке упрутся в CSRF.
- Строка 207 `cors: [siteURL]` → `cors: [siteURL, 'https://cms.shumiland.com.ua']`.
- Добавить рядом `csrf: [siteURL, 'https://cms.shumiland.com.ua']` (сейчас csrf не задан — по умолчанию = serverURL).
### B.5. Чистка хардкодов старого домена / рассинхрона TLD
- `next.config.ts:14` — удалить remotePattern `shumi.ai-impress.com` (строка 13 уже содержит `shumiland.com.ua`).
- Привести fallback-дефолты `shumiland.ua` → `shumiland.com.ua` (срабатывают только если env пуст, но выравниваем):
- `src/lib/resend.ts:12` (`noreply@shumiland.ua`)
- `src/seed.ts:212` (`https://shumiland.ua`), `:28`/`:35` (`admin@shumiland.ua`)
- `src/app/api/admin/seed/route.ts:83`,`:90` (`admin@shumiland.ua`)
- ожидания в `tests/unit/lib/resend.test.ts`, `tests/unit/hooks/revalidatePath.test.ts`
> Не трогать: `.claude/worktrees/...` (дубликат-воркпрейс), `.claude/settings.local.json` (allowlist разрешений Claude, не runtime).
---
## Часть C — Действия на VPS (`/opt/shumiland`)
Конфиги Caddyfile / docker-compose.prod.yml / .env.production на VPS **не синхронизируются CI** (CI делает только `docker compose pull app` + `up app`). После push:
1. Обновить файлы на VPS (git pull в `/opt/shumiland`, либо scp Caddyfile + docker-compose.prod.yml).
2. Отредактировать `/opt/shumiland/.env.production`:
- `NEXT_PUBLIC_SITE_URL=https://shumiland.com.ua`
- `RESEND_FROM=noreply@shumiland.com.ua`
- `RESEND_API_KEY=<боевой ключ Resend>` (см. Часть E)
- `MANAGER_EMAILS=<кому слать лиды, через запятую>`
- `CERTBOT_DOMAIN=shumiland.com.ua` (если ещё используется скриптами)
3. Положить **Cloudflare Origin Cert** в `/opt/shumiland/certs/origin.pem` + `origin-key.pem` (из A.5).
4. Применить (после того как `cms.shumiland.com.ua` уже резолвится на `147.135.209.100`):
```bash
cd /opt/shumiland
docker compose -f docker-compose.prod.yml up -d caddy app
docker compose -f docker-compose.prod.yml logs -f caddy # дождаться выдачи Let's Encrypt cert для cms
```
`app` подхватит новый `NEXT_PUBLIC_SITE_URL` из `.env.production` при рестарте. Свежий образ с client-side URL придёт после push в `main` (CI build → pull). Можно дождаться зелёного workflow до шага 4.
---
## Часть D — Verification (end-to-end)
1. `dig +short shumiland.com.ua` → адреса Cloudflare; `dig +short cms.shumiland.com.ua` → `147.135.209.100`.
2. `curl -I https://shumiland.com.ua` → `200`, валидный cert (без `-k`). В заголовках `server: cloudflare` / `cf-ray`.
3. `curl -I https://cms.shumiland.com.ua` → `200`, валидный Let's Encrypt cert, **без** `cf-ray` (напрямую на origin).
4. `curl -I https://www.shumiland.com.ua` → редирект на apex.
5. Браузер `https://shumiland.com.ua` — главная грузится, картинки (`/api/media/...`) отдаются; `/admin` и `/api/*` не закэшированы (`cf-cache-status: BYPASS/DYNAMIC`).
6. `https://cms.shumiland.com.ua/admin` — залогиниться заново (сессии host-only). **Загрузить видео >100 MB** → проходит.
7. SEO canonical в `<head>` → `shumiland.com.ua` (`payload.config.ts:119`).
8. Тестовый лид через форму → письмо с `noreply@shumiland.com.ua`, в заголовках SPF/DKIM = pass.
9. `shumi.ai-impress.com` больше не отдаёт сайт.
---
## Часть E — Resend (верификация домена + почта)
1. **Resend dashboard → Domains → Add Domain** → `shumiland.com.ua`. Выбрать регион (EU ближе к UA).
2. Resend выдаст **набор DNS-записей** — добавить их **в Cloudflare** (все как **DNS only / для TXT-MX прокси не применяется**). Точные имена/значения брать из дашборда Resend, не хардкодить. Обычно:
- **MX** на `send.shumiland.com.ua` → `feedback-smtp.<region>.amazonses.com` (priority 10) — обработка bounce.
- **TXT SPF** на `send.shumiland.com.ua` → `v=spf1 include:amazonses.com ~all`.
- **TXT DKIM** на `resend._domainkey.shumiland.com.ua` → выданный публичный ключ.
- (опц.) **TXT DMARC** на `_dmarc.shumiland.com.ua` → `v=DMARC1; p=none;`.
3. В Resend нажать **Verify** — дождаться, пока все записи зелёные (Verified).
4. **API key**: создать/взять production-ключ Resend → в `.env.production` как `RESEND_API_KEY`.
5. `RESEND_FROM=noreply@shumiland.com.ua` (адрес обязан быть на верифицированном домене).
6. `MANAGER_EMAILS` — получатели уведомлений о лидах.
7. Код: `src/lib/resend.ts` (`sendLeadAlert`) и `payload.config.ts:49` (`defaultFromAddress`) уже читают `RESEND_FROM` с дефолтом на домен — после B.5 дефолты выровнены на `.com.ua`.
---
## Часть F — Опциональный хардненинг и rollback
**Хардненинг (по желанию, после успешного переключения):**
- На VPS закрыть firewall'ом порты 80/443 для всех, кроме [IP-диапазонов Cloudflare](https://www.cloudflare.com/ips/) — origin не дёргают в обход прокси. **Осторожно:** `cms` (DNS-only) должен остаться доступен напрямую — не блокировать его трафик.
- Реальный IP клиента за прокси: если в лид-форме/логах важен IP — настроить доверие заголовку `CF-Connecting-IP` (Caddy `trusted_proxies` cloudflare + чтение в Next.js). Для маркетинг-сайта обычно некритично.
**Rollback (если что-то пошло не так):**
- DNS: вернуть в nic.ua прежние NS hostiq (`dns1/dns2.hostiq.ua`) — сайт снова резолвится по-старому (с учётом TTL).
- App: задеплоить предыдущий образ — `TAG=<old-sha> docker compose -f docker-compose.prod.yml up -d app`.
- `.env.production`: вернуть прежний `NEXT_PUBLIC_SITE_URL`.
- Caddyfile: вернуть прежнюю версию (self-signed/IP) из git history.
---
## Сводка файлов к изменению
| Файл / место | Изменение |
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `Caddyfile` | Публичный домен на Origin Cert + `cms` на Let's Encrypt |
| `docker-compose.prod.yml` | build-arg URL; убрать `NODE_EXTRA_CA_CERTS` + certs-mount у `app`; добавить caddy volumes |
| `payload.config.ts` | `cms.shumiland.com.ua` в `cors` + `csrf` |
| `.github/workflows/deploy.yml` | `build-args: NEXT_PUBLIC_SITE_URL` |
| `next.config.ts` | Удалить remotePattern старого домена |
| `src/lib/resend.ts`, `src/seed.ts`, `src/app/api/admin/seed/route.ts`, тесты | `shumiland.ua` → `shumiland.com.ua` |
| `/opt/shumiland/.env.production` (VPS) | URL, Resend, manager emails |
| `/opt/shumiland/certs/origin.pem` + `origin-key.pem` (VPS) | Cloudflare Origin Cert |
| Cloudflare dashboard | NS, A-записи (apex/www proxied, cms DNS-only), Full strict, Origin Cert, Cache Rules, Redirect Rule |
| Resend dashboard + Cloudflare DNS | Add domain, MX/SPF/DKIM/DMARC, Verify, API key |
## Что НЕ требует изменений
- `serverURL` / `cors` (apex) / `livePreview` — выводятся из `NEXT_PUBLIC_SITE_URL` автоматически. Меняем только добавление `cms`-хоста в cors/csrf.
- Cookie domain — не задан, куки host-only; нужен только повторный логин в админку.