Кейс01 / 06
Dinastia
Маркетплейс породистых животных Молдовы · RO/RU
Мой собственный продукт, от названия и логотипа до базы данных. Это двуязычный каталог, где заводчики и владельцы размещают породистых собак и кошек, а покупатель оставляет заявку прямо со страницы питомца.
- Роль
- Основатель · дизайн и full-stack
- Период
- авг. 2026 – сейчас
- Контекст
- Свой продукт
- Формат
- В одиночку · Кишинёв
- Next.js App Router
- React 19
- Supabase (Postgres, Auth, Storage)
- Row Level Security
- next/image
- Resend
- Telegram Bot API
- Vercel
- node:test


Задача
Породистых щенков и котят в Молдове продают в основном через разрозненные объявления и соцсети, где сложно проверить документы и продавца. Мне хотелось сделать каталог, которому можно доверять. Он должен был ранжироваться в Google по запросам «порода + город» и работать на румынском и русском. Заводчики ведут объявления сами, но обойти модерацию не могут.
- Быстрые индексируемые страницы для каждой породы и города
- Кабинеты заводчиков, где всё проходит модерацию до публикации
- Заявки сразу доходят до человека, потому что рынок живёт звонками
- Персональные данные (сканы документов, телефоны) не попадают в публичный HTML
Что я сделал
Каталог и SEO
- Next.js App Router с префиксами /ro и /ru. Один динамический сегмент обслуживает и лендинги пород, и лендинги городов (11 пород собак, 7 пород кошек, городские страницы).
- Статическая генерация через generateStaticParams и ISR раз в час. За сборку выходит около 90 статических страниц.
- Вся микроразметка schema.org собирается одним хелпером. У питомца Product + Offer, у страницы породы AggregateOffer. Ещё есть BreadcrumbList, ItemList, FAQPage, BlogPosting и Organization или Person для продавца.
- Sitemap с реальными датами lastmod и hreflang RU/RO. Слабые объявления (без фото или с коротким описанием) в него не попадают.
- Middleware делает один 308-редирект сразу на финальный адрес, без цепочек.
Кабинет заводчика и админка
- Редактор объявлений со статусами draft → pending → published / rejected / archived. Причины отклонения видны в кабинете.
- Фото сжимаются в браузере ещё до загрузки. Ресайз идёт через canvas, на выходе WebP с запасным JPEG и не больше 200 КБ.
- В модерации есть статусы, проверка документов по временным подписанным ссылкам, продавцы, настройки сайта и статистика.
- Ревалидация кэша по тегам после модерации и ежедневный cron, который удаляет аккаунты после окончания срока, отведённого на отмену удаления.
Заявки и контакты
- /api/lead проверяет данные, отсекает ботов через honeypot-поле и пропускает 5 запросов в час с одного IP. Сервер сам находит питомца и продавца, отправляет заявку в Telegram, записывает её в журнал заявок и уведомляет заводчика.
- Телефона продавца нет в HTML. Он подгружается по клику через /api/contact, не больше 20 запросов за 10 минут с одного IP.
- Лимиты считаются в Postgres одним атомарным upsert, поэтому работают на нескольких serverless-инстансах.
Результат
- органических кликов из Google за первые 2 месяца (по Search Console)
- ~500
- в Google по «купить породистого кота / питомца» в Молдове (окт. 2026)
- Топ 3–4
- статически сгенерированных страниц
- ~90
- языка, связанных через hreflang
- 2
Бренд и логотип тоже мои. Самые рискованные места покрыты тестами на node:test. Они проверяют экранирование JSON-LD, безопасные редиректы, загрузку документов, контакты продавца и авторизацию cron.
Модель безопасности
В браузере есть только публичный ключ Supabase, поэтому что кому можно, решает сама база.
- Row Level Security на всех таблицах. Сначала у ролей anon и authenticated отозваны все права по умолчанию.
- Права выданы на уровне колонок. Продавец может менять имя, город, описание и контакты, но не флаг verified, статус, featured или slug.
- Триггер guard_publish отклоняет перевод в статус «published», если запрос пришёл не от серверной роли. Политика видит только новую строку, а триггер видит и старую, и новую.
- Политики удаления нет. Объявления архивируются, потому что проиндексированная страница с 404 хуже, чем «продано».
- Сканы родословных и ветпаспортов лежат в приватном бакете без политик чтения. Модератор открывает их по временным подписанным ссылкам.
- Счётчики и лимиты работают через функции SECURITY DEFINER, которые может вызвать только сервер.