Вектор пола — магазин напольных покрытий
Действующий интернет-магазин напольных покрытий: ламинат, кварцвинил и SPC, инженерная и паркетная доска — у заказчика два салона, в Москве и Красногорске. В каталоге около 7 600 активных товаров: 3 300 позиций ламината, 3 900 кварцвинила и SPC, плюс паркетная и инженерная доска. Витрина написана на чистом PHP, без CMS и фреймворков, работает на дешёвом shared-хостинге и не требует ни одной сборки. Ключевое инженерное решение — двухслойное хранилище: канонический JSON на 17,5 МБ и производный SQLite-кеш, из которого витрина и читает. Заказы идут не через онлайн-оплату, а заявками в Telegram — так устроен процесс продаж у заказчика.
Архитектура данных
Ключевая идея: products.json — источник истины, catalog.sqlite — производный read-кеш. Витрина больше не декодирует 17,5 МБ JSON на каждый запрос, а обращается к готовой базе через PDO.
- Админка пишет в JSON, а база пересобирается один раз за запрос через register_shutdown_function — это критично для импорта, который сохраняет товары в цикле и иначе запускал бы пересборку на каждой строке.
- Пересборка атомарная: пишем в catalog.sqlite.tmp и подменяем через rename. При ошибке временный файл удаляется, а старая база остаётся живой — живые запросы никогда не попадают на полузаписанную базу.
- Две таблицы: products (22 колонки, флаги active/in_stock/popular/promo и поле search_text = lower(name + brand) для регистронезависимого поиска по кириллице) и product_facets с развёрнутыми значениями фильтров по каждому товару. Пять индексов на товарах, три на фасетах.
- Устойчивость к рассинхрону схемы: запросы к новым колонкам обёрнуты в try/catch. Если PHP залит раньше пересборки базы, ни витрина, ни админка не падают в 500 — деградируют мягко, пустым блоком или откатом на JSON.
Каталог и фильтры
Около 7 600 активных товаров в 4 наполненных категориях из 9; остальные показывают состояние «Раздел наполняется» вместо пустой страницы.
- Фасетные фильтры сконфигурированы под реальные характеристики каждой категории: бренд, класс истираемости, толщина, тип укладки, порода, покрытие. Логика — ИЛИ внутри фасета и И между фасетами, у каждого значения счётчик товаров.
- Диапазон цены с подсказкой реальных границ по категории, фильтр «только в наличии», сортировка по цене и серверная пагинация по 24 товара.
- Авто-сабмит фильтров: на десктопе — сразу по клику, в мобильной шторке — по кнопке «Показать», цена — по Enter.
- Поиск по названию и бренду с мультисловными запросами: каждое слово должно найтись в нормализованном search_text.
- Данные каталога отнормализованы: без нулевых цен и дублей slug, бренды проставлены, ключи характеристик канонизированы.
Карточка товара и заказ
У напольных покрытий своя специфика: товар продаётся упаковками, а покупатель считает в квадратных метрах.
- Калькулятор площади: вводишь метраж — считает количество упаковок с округлением вверх, фактическую площадь и итоговую сумму. Первый расчёт отрисован на сервере, поэтому карточка осмысленна и с отключённым JavaScript.
- Галерея с переключением фото, характеристики таблицей, кнопки «В корзину» и «Купить в один клик», ссылки «поделиться» в WhatsApp и Telegram и копирование адреса.
- Корзина на клиенте (localStorage) с пересчётом упаковок по каждой позиции; оформление отправляет заявку, а не платёж — состав заказа и сумма уходят в Telegram текстом.
- Двухслойная заглушка для фото: пустой images — фолбэк на сервере, битая ссылка — onerror на клиенте.
Заявки и уведомления
- Все формы — консультация, покупка в один клик, корзина, доставка, сотрудничество с дизайнерами — идут через один обработчик в Telegram-бота, с пометкой источника заявки.
- Раздельные таймауты на коннект и на всю операцию (5 и 15 с). При сбое API заявка дописывается в закрытый лог data/leads-failed.log — лид не теряется, даже если Telegram недоступен.
- Подписка на акции пишется в JSON под flock, с дедупликацией по e-mail: повторная отправка обновляет имя, телефон и дату, а не плодит записи.
- На клиенте — единая инициализация форм, которая читает эндпоинт и текст успеха из data-атрибутов; маска телефона и валидация переиспользуются всеми формами.
Кастомная админ-панель
Полностью самописная, за логином на PHP-сессиях.
- Каталог: CRUD товаров, серверные поиск и пагинация, вкладки «Список», «Популярные», «Акционные» — галки в карточке товара управляют каруселями на главной, а пустая секция просто не рендерится.
- Импорт и экспорт XLSX через собственные читалку и писалку на ZipArchive и ручном XML — без Composer и внешних библиотек. Экспорт разворачивает все ключи характеристик в отдельные столбцы, шаблон отдаётся со строкой-примером, фиксированные и дополнительные столбцы подкрашены разным цветом.
- Импорт делает upsert: сначала по slug, затем по артикулу; чинит артикулы вида «1.0», которые Excel и Google Таблицы делают из целых чисел, и умеет подтягивать фото по ссылке.
- Загрузка изображений: центральный кроп в квадрат 800×800 и конвертация в WebP на сервере (GD), приём и файлом, и по URL, с проверкой MIME.
- Подписки: список с новыми сверху, удаление через POST с подтверждением и PRG-редиректом, выгрузка в CSV (с BOM для Excel) и XLSX.
- Защита входа: арифметическая капча, блокировка на 5 минут после 5 неудачных попыток, восстановление пароля в Telegram и на e-mail. Ручная пересборка базы — отдельной страницей.
SEO
- Динамический sitemap.xml прямо из SQLite: статика, только непустые категории и все активные товары с lastmod из даты обновления — на сегодня порядка 7 600 URL.
- Schema.org / JSON-LD: Product + Offer с priceValidUntil, itemCondition и динамической доступностью на карточках, BreadcrumbList на карточках, категориях и индексе каталога.
- Осознанное управление индексацией: чистая пагинация индексируется с self-canonical и rel prev/next, а комбинации фильтров и сортировок уходят в noindex,follow — чтобы не плодить дубли.
- SEO-title и description задаются по каждому товару в админке, с фолбэком на шаблон из названия. Человекочитаемые URL /catalog/{категория}/{slug}/ через mod_rewrite, кириллица транслитерируется в slug с гарантией уникальности.
- Внутренний поиск и корзина закрыты от индексации, robots.txt и sitemap согласованы между собой.
Производительность и хостинг
Скорость вытянута архитектурой хранилища, а не инфраструктурой: ни CDN, ни кеш-слоя, ни фоновых демонов — дешёвый shared-хостинг Beget и заливка по rsync.
- Отказ от разбора 17,5 МБ JSON на каждый запрос в пользу индексированных SQL-выборок — главный выигрыш по времени ответа.
- Все ~7 800 товарных фото приведены к единому WebP 800×800, логотипы брендов — к WebP 320×180. Пайплайн подготовки — локальные скрипты на Python/PIL с обрезкой белых полей и ограничением апскейла.
- Preload и fetchpriority на hero-изображении (LCP), ленивая загрузка остальных картинок, явные width/height — нулевой сдвиг макета.
- Gzip и заголовки кеширования статики в .htaccess, cache-busting ассетов через ?v=N.
- Аналитика — Яндекс.Метрика с webvisor и clickmap.
Безопасность и надёжность
- Папка /data/ с товарным JSON и базой подписчиков закрыта и в .htaccess (RewriteRule с [F]), и в robots.txt; config.php с токеном бота запрещён к прямому вызову.
- В /uploads/ отключено исполнение PHP и разрешены только изображения; служебные файлы админки закрыты через FilesMatch, и каждый дополнительно сам проверяет, что его вызвали не напрямую.
- Осмысленные 404 для несуществующих товаров и категорий: проверяется не только slug, но и совпадение категории в URL с категорией самого товара.
Чем интересен проект
- Каталог на 7 600 товаров работает на shared-хостинге без CMS, без MySQL, без npm и Composer — за счёт продуманного хранилища, а не мощностей.
- JSON как источник истины и SQLite как производный read-кеш: редкая запись, частое чтение — компромисс, который для этого проекта оказался точнее полноценной СУБД.
- Инженерные детали, которые видно только в эксплуатации: атомарная пересборка базы, одна пересборка на запрос вместо тысяч при импорте, мягкая деградация при рассинхроне схемы, фолбэк-лог для потерянных заявок.
- Свои XLSX-читалка и писалка вместо тяжёлой библиотеки — заказчик ведёт ассортимент в Excel, и это работает без единой внешней зависимости.
- Полный цикл одним человеком: архитектура данных → бэкенд → фронтенд → админка → пайплайн изображений → SEO → деплой.