Реальный кейс: abhazservis.com — каталог из 273 организаций за девять дней
Что это
abhazservis.com — каталог организаций Абхазии: отели, рестораны, транспорт, аптеки, банки, магазины, точки связи. На русском языке и для тех, кто живёт там, а не только для туриста в августе.
Это не заказ клиента. Это собственный проект GeoMachine, и рассказываю я о нём именно поэтому: здесь можно показать решения целиком, включая те, что оказались неверными, ни у кого не спрашивая разрешения.
Цифры на сегодня: 273 опубликованные карточки, 19 категорий, 8 населённых пунктов, 44 карточки, проверенные по официальному сайту самой организации. Ноль выручки, ноль карточек, заявленных владельцами, и ноль накопленной статистики посещаемости: каталог построен, коммерческая часть ещё не начиналась. Обновление: сегодня в каталоге уже 274 опубликованные организации.
От нуля до админ-панели за девять дней
Первый коммит — 2 сентября 2026 года. Админ-панель закрыта 10-го. Девять календарных дней на: инфраструктуру, аутентификацию, модель данных, пакетный импорт, панель каждой организации, поиск и публичную карточку, самостоятельную регистрацию бизнеса и админ-панель.
Это не скорость набора текста. Каждый блок сначала проектировался письменно и только потом писался, и каждый закрывался тестами против настоящей базы PostgreSQL, без заглушек. Первые шесть блоков были готовы за двое суток именно потому, что трудная работа — решить, как устроена схема данных, — была уже сделана.
Стек и его обоснование
- Astro 7 в серверном режиме (адаптер Node, standalone) для публичной части.
- Express 5 как отдельный backend, монорепозиторий из трёх пакетов:
shared,backend,web. - PostgreSQL 17, 19 таблиц, миграции на чистом SQL с обязательным откатом.
- Node 24 и TypeScript в строгом режиме, vitest против настоящего Postgres.
- Размещение — собственный мини-ПК за туннелем Cloudflare. Postgres и backend не публикуют ни одного порта; сайт слушает только локальный интерфейс.
Самое обсуждаемое решение было таким: каталог живёт за счёт двух вещей — попадания в поиск и скорости загрузки. Astro по умолчанию отдаёт ноль JavaScript, и публичные страницы отдаются без единого гидратированного острова. Backend отдельно, потому что пакетный импорт вызывается скриптом с API-ключом, а не из чьего-то браузера.
И два решения не делать: без PostGIS (240 км побережья не нуждаются в настоящей геометрии, хватает двух колонок с широтой и долготой) и без слоя интернационализации (сайт русскоязычный, второй язык не планируется, а городить i18n «на всякий случай» — это платить за сложность вперёд).
В URL никогда нет категории
Адрес каждой карточки — /{населённый-пункт}/{название}. Категории в нём нет.
Причина конкретная: смена категории не должна ломать адрес и накопленный поисковый вес. А категории меняются чаще, чем кажется, — когда каталог вырос с 8 категорий до 19, 32 карточки переехали, и ни одна не сменила URL. Категория показывается «хлебными крошками» и вычисляется при отрисовке страницы.
Если у вас каталог любого рода, это то решение, за которое вы скажете себе спасибо через два года.
Ошибка, которая ломала все формы
Она заслуживает отдельного раздела, потому что случится с каждым, кто поставит сайт на Astro за HTTPS-прокси — туннель Cloudflare, Nginx с Let’s Encrypt, любой.
В Astro есть защита от отправки форм с чужого сайта: заголовок Origin из браузера сверяется с origin самой страницы. Проблема в том, как этот origin вычисляет адаптер Node: он смотрит, зашифрован ли сокет. А за прокси он не зашифрован — HTTPS заканчивается на прокси, и до сервера доходит обычный HTTP. Заголовок X-Forwarded-Proto, который существует ровно для того, чтобы сказать «это пришло по HTTPS», не читается вообще.
Итог: браузер шлёт Origin: https://…, сервер считает, что его origin — http://…, они не совпадают, и любой POST отвечает 403. Сайт выглядит идеально, и ни одна форма не работает.
Доказательством стал один и тот же запрос к одному и тому же серверу с изменением только этого заголовка: с https → 403, с http → 200. Решение — отключить встроенную проверку Astro и переписать её в своей прослойке, которая X-Forwarded-Proto читает.
Рассказываю об этом потому, что симптом обманчив: выглядит как проблема туннеля, а это не она. Изначальный вариант с Nginx и собственными сертификатами сломался бы точно так же.
День, когда тесты стёрли каталог
В тот же день, когда были импортированы первые 168 карточек, они исчезли. Дважды.
Причина: набор тестов очищал таблицы перед каждой проверкой — как и должен, — но брал адрес базы из той же переменной окружения, что и рабочее приложение. Один npm test не на той машине, и каталога нет.
Починка двухслойная, и вот что здесь важно: одного слоя не хватало. Первый — отдельная переменная окружения для тестов. Второй — защита, которая отказывается очищать любую базу, чьё имя не заканчивается на _test, что бы ни было в переменных. Первый слой можно забыть настроить; второй ни от чьей памяти не зависит.
Всё восстановилось повторным импортом исходных пакетов, которые лежали вне репозитория. Это третий слой, скучный: исходные данные хранятся и после загрузки.
Что нашла проверка и чего никто не видел
При построении порядка вывода организаций внутри категории сверка с настоящей базой показала, что четырёх критериев сортировки из пяти не существует: колонок, которые они запрашивали, в схеме не было. Фактический порядок был чисто алфавитным — и был им неделями, незаметно.
На замену пришла формула «полноты карточки» — фото, описание, проверка, сайт или соцсети, телефон, с разными весами, — которая применяется всегда внутри одной категории, никогда не сравнивая аптеку с отелем.
Отрицательный результат, который тоже ценен
Пробовали генерировать описания карточек автоматически из структурированных полей. Два подхода, второй — с явными контрпримерами того, чего не надо. Результат: 2 принятых из 11, и обе были из одной формы с заменёнными синонимами.
Вывод: автоматического конвейера описаний не будет. Знать это до того, как строить его на две тысячи карточек, дороже, чем те описания, которые он бы выдал.
Чего не хватает
Отзывов и избранного, формального блока SEO и формы обратной связи с учётом обращений. Платная премиум-карточка имеет смысл только тогда, когда есть реальная статистика, которую можно показать владельцу, — поэтому таблица ежедневной статистики существует с первого дня, хотя продажа относится к более поздней фазе. Если завести её к началу продаж, первый квартал продаётся без единой цифры на руках.
Посмотреть сайт вживую: abhazservis.com