На предыдущем шаге мы собрали лендинг с пятью секциями. Форма в секции CTA выглядит как настоящая, но пока ничего не делает — данные никуда не отправляются. Исправим это: подключим базу данных и научим форму сохранять заявки.
Лид — это человек, который оставил свои контактные данные. Заполнил форму, нажал кнопку — и стал потенциальным клиентом. До этого он был просто анонимным посетителем.
Задача лендинга — превратить как можно больше посетителей в лидов. Поэтому форма заявки — ключевой элемент. Чем проще форма, тем больше людей её заполнят. Для начала достаточно трёх полей: имя, контакт (email или телефон) и согласие на обработку данных.
Данные хранятся в базе данных — специальной программе, которая умеет надёжно сохранять, искать и обновлять информацию. Мы используем PostgreSQL — одну из самых популярных баз. Она уже работает в нашем Docker-контейнере, мы настроили это на шаге 0.
Базы данных общаются на специальном языке — SQL. Он мощный, но не очень удобный: нужно помнить синтаксис, следить за безопасностью запросов, вручную описывать структуру. Чтобы не работать с SQL напрямую, мы используем Prisma ORM — современный инструмент, который берёт это на себя. Мы описываем структуру данных в понятном формате, а Prisma сама генерирует запросы, следит за типами и под капотом закрывает уязвимости (например, SQL-инъекции — когда злоумышленник пытается выполнить свой код через форму).
Прежде чем просить агента писать код, нужно понять, какие данные мы будем хранить. База данных — это таблицы, похожие на таблицы в Excel. У каждой таблицы есть столбцы (поля) и строки (записи). Но в отличие от Excel, у каждого поля есть строгий тип и правила.
Несколько ключевых понятий:
id, которое база создаёт автоматически. По нему можно найти любую записьvisitor_id в таблице Lead указывает на конкретного посетителя в таблице Visitor. Так таблицы связаны между собой?. Например, не у каждого события есть связанная заявка, поэтому lead_id может быть пустымДля нашего лендинга нужны три таблицы:
| Поле | Тип | Описание |
|---|---|---|
| idPK | Int | Уникальный номер записи, создаётся автоматически |
| fingerprintunique | String | Отпечаток браузера — для определения уникальных посетителей |
| first_seen_at | DateTime | Когда впервые зашёл на сайт |
| ip | String | IP-адрес посетителя |
| user_agent | String | Информация о браузере и устройстве |
| Поле | Тип | Описание |
|---|---|---|
| idPK | Int | Уникальный номер заявки |
| namerequired | String(100) | Имя — до 100 символов |
| contactuniquerequired | String(255) | Email или телефон — уникальный, одна заявка на контакт |
| consentrequired | Boolean | Согласие на обработку данных — обязательно |
| visitor_idFK → Visitor | Int | Какой посетитель оставил заявку |
| created_at | DateTime | Когда заявка была создана |
| Поле | Тип | Описание |
|---|---|---|
| idPK | Int | Уникальный номер события |
| typerequired | String | Тип: landing_view, cta_click, lead_created, webhook |
| sourcerequired | String | Откуда: internal (с сайта) или external (webhook) |
| visitor_idFK → Visitor | Int? | С каким посетителем связано (может быть пустым) |
| lead_idFK → Lead | Int? | С какой заявкой связано (может быть пустым) |
| idempotency_keyunique | String? | Ключ для защиты от дублей (только для webhook) |
| payload | JSON | Дополнительные данные события |
| created_at | DateTime | Когда событие произошло |
Visitor — уникальный посетитель сайта. Каждый, кто зашёл на страницу, получает запись.
Lead — кто оставил заявку. Поле contact помечено как unique — один email не может создать две заявки. Поле visitor_id — это Foreign Key, ссылка на таблицу Visitor.
EventLog — журнал всех событий. Подробно разберём на следующем шаге. Сейчас главное — создать структуру, чтобы было куда записывать. Обратите внимание на поле idempotency_key — это уникальный ключ, который защищает от дублирования событий. Если кто-то отправит одно и то же событие дважды, база не даст создать дубль. Мы подробнее поговорим об этом в следующей главе.
Может показаться, что достаточно одной таблицы для заявок. Но разделение даёт нам важные возможности:
Связи между таблицами (Foreign Keys) позволяют задавать вопросы вроде: «Покажи все события этого посетителя» или «Какой посетитель оставил эту заявку». Без связей пришлось бы хранить всё в одной гигантской таблице и разбираться в каше данных.
Когда данные приходят от пользователей, нужно установить правила — что допустимо, а что нет:
Prisma и React берут на себя основную защиту: Prisma защищает от SQL-инъекций (когда злоумышленник пытается выполнить свой код через форму), а React автоматически очищает данные перед показом на странице — чтобы никто не смог вставить вредоносный код через поле ввода.
Форма работает в браузере пользователя, а база данных — на сервере. Браузер не может подключиться к базе напрямую — это было бы небезопасно. Поэтому между ними есть промежуточный шаг: форма отправляет данные на сервер, и уже сервер записывает их в базу.
Выглядит это так: пользователь нажимает «Отправить» → браузер отправляет данные на адрес вроде /api/leads → сервер проверяет и сохраняет → отвечает браузеру «Спасибо» или «Что-то не так». Это всё внутри нашего приложения, никаких отдельных сервисов.
Когда мы описываем таблицы в Prisma, это ещё не создаёт их в базе данных. Чтобы структура применилась, нужно запустить миграцию — команду, которая создаст или обновит таблицы. Каждая миграция — это отдельный шаг: «добавить таблицу Lead», «добавить поле status». Prisma сама генерирует миграции и хранит их историю, чтобы на любом компьютере база выглядела одинаково.
Попросите агента подключить базу данных и научить форму сохранять заявки:
У меня есть лендинг с формой заявки (имя, контакт,
согласие на обработку данных). Нужно подключить
базу данных и сделать так, чтобы форма сохраняла
заявки.
Создай три таблицы через Prisma:
1. Посетители (Visitor) — каждый, кто зашёл
на сайт. Храни уникальный отпечаток браузера,
дату первого визита, IP-адрес и информацию
о браузере и устройстве.
2. Заявки (Lead) — кто оставил контакт.
Имя (до 100 символов), контакт — email
или телефон (до 255 символов, уникальный,
чтобы один человек не мог отправить дважды),
согласие (обязательное поле), ссылка
на посетителя и дата создания.
3. Журнал событий (EventLog) — всё, что
происходит на сайте. Тип события, откуда
пришло (с сайта или извне), ссылки
на посетителя и заявку (могут быть пустыми),
ключ для защиты от дублей (для внешних
событий), данные события и дата.
Когда пользователь отправляет форму, данные
должны отправляться на сервер, проверяться
там и сохраняться в базу. При успехе — показать
"Спасибо, мы свяжемся с вами". При ошибке —
показать что пошло не так.
Примени миграцию базы данных.
Также создай скрипт scripts/migrate.sh, который
подключается к серверу по SSH и применяет миграции
внутри Docker-контейнера. Параметры (IP, ключ,
пользователь) — как аргументы при запуске.
Сделай скрипт исполняемым.
После деплоя, когда структура базы меняется, нужно применить миграции на сервере:
user@computer:~/landing$ ./scripts/migrate.sh <ваш-IP> ~/.ssh/my-project deployПосле того как агент создаст модель и подключит форму, перезапускаем контейнеры:
user@computer:~/landing$ docker compose up --buildОткройте лендинг, прокрутите до формы и отправьте тестовую заявку. Если всё работает — вы увидите сообщение об успехе.
Чтобы проверить данные в базе, попросите агента создать скрипт, который скачает все таблицы в CSV-файлы — их можно открыть в Excel или Google Sheets:
Создай скрипт scripts/db-export.sh, который
подключается к серверу по SSH, выгружает все
таблицы из базы данных в CSV-файлы и сохраняет
их в папку data/ на локальном компьютере.
Параметры (IP, ключ, пользователь) — как
аргументы при запуске. Параметры подключения
к базе бери из .env. Сделай скрипт исполняемым.
После отправки тестовой заявки запускаем:
user@computer:~/landing$ ./scripts/db-export.sh <ваш-IP> ~/.ssh/my-project deployВ папке data/ появятся CSV-файлы — откройте их в Excel или любом текстовом редакторе и убедитесь, что ваша заявка на месте, а также остальные поля во всех таблицах заполнены. При необходимости попросите агента исправить. Например, типичная ошибка агента — пустой idempotency_key в таблице event_log. Это означает, что может происходить дублирование событий.
Сначала деплоим код на сервер:
user@computer:~/landing$ ./scripts/deploy.sh <ваш-IP> ~/.ssh/my-project deployЗатем применяем миграции — чтобы на сервере создались таблицы в базе данных:
user@computer:~/landing$ ./scripts/migrate.sh <ваш-IP> ~/.ssh/my-project deployПосле этого откройте сайт по IP-адресу и отправьте заявку — убедитесь, что на сервере всё работает так же, как локально.
Перед коммитом попросите агента обновить README:
Обнови README.md — добавь раздел про базу данных:
какие таблицы есть, как применить миграции,
как выгрузить данные в CSV.
После этого сделай коммит.
Форма работает и сохраняет заявки. Но мы пока не знаем, сколько людей зашло на сайт, сколько кликнуло на кнопку, и какой процент оставил заявку. В следующей главе добавим события конверсии и webhook inbox.