Кейс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
Открыть сайтdinastia.mdКод по запросу
Главная страница Dinastia на десктопе
Главная страница Dinastia на телефоне

Задача

Породистых щенков и котят в Молдове продают в основном через разрозненные объявления и соцсети, где сложно проверить документы и продавца. Мне хотелось сделать каталог, которому можно доверять. Он должен был ранжироваться в 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, которые может вызвать только сервер.