Витрина не ходит в базу на каждом запросе. Она читает JSON, который собрала админка. Это быстро, просто и иногда очень больно.
Почему без API
Когда я начинал, у меня не было цели построить «правильную» архитектуру. Была цель — сделать сайт, который я смогу поддерживать один, без DevOps-команды и без зоопарка сервисов.
Поэтому получилось так:
- SQLite — только в админке. Одна файловая база, WAL, никакого отдельного сервера.
- PHP — только там, где нужна запись или закрытая страница: кабинет, профили, статьи, проекты.
- JSON — публичный слой. Сайт читает готовые файлы из
data/, а не подключается к базе. - Vanilla JS — рендер карточек, фильтры, поиск, статьи. Без React, без сборки, без API-слоя.
Мне нравится эта схема. Статическая витрина не может уронить базу, потому что она к ней не подключена. JSON можно открыть, продиффрить, положить в git или отдать в кэш. Для небольшого портала этого достаточно.
Но у схемы есть цена: два мира. И между ними появляются призраки.
Как данные ходят по кругу
Путь такой:
- Я что-то меняю в админке: проект, статью, участника, бейдж.
- PHP пишет в SQLite.
- После изменения вызывается экспорт:
export_all()собирает публичные срезы. - В
data/появляютсяprojects.json,articles.json,team.json,blog.jsonи остальные файлы. - Сайт читает JSON и рисует интерфейс.
Внутри экспорта нет магии. Это обычные SELECT, сборка массива, json_encode. Например, для статей выбираются только опубликованные, для команды — только активные участники, для проектов — только публичные поля.
Условно:
$rows = $pdo->query("SELECT ... FROM posts WHERE status = 'published'")->fetchAll();
file_put_contents($root . '/data/articles.json', json_encode($rows, JSON_UNESCAPED_UNICODE));
Всё. Никакого брокера, очередей и «источника событий».
Главное правило: база — источник, JSON — проекция
Это звучит очевидно, но именно на этом ломаются.
SQLite — единственный источник правды. Всё, что я хочу сохранить, живёт в базе. JSON — производная. Его можно удалить и собрать заново. Его нельзя править руками «по-быстрому», потому что следующий экспорт затрёт правку. И его нельзя считать независимым хранилищем.
Если это правило нарушить, начинается:
- В базе запись удалена, а в JSON она осталась — потому что экспорт не запустился.
- В JSON поправили заголовок, а в админке он старый — потому что база не знает о правке.
- На dev и prod разные JSON, потому что экспорт делали в разных местах.
- Браузер держит старый файл, и сайт показывает то, чего уже нет.
Самый неприятный вариант — призрак данных: запись уже удалена, но всё ещё видна на сайте. Или наоборот: запись есть, но её нигде нет, потому что экспорт упал.
Где мы наступили на грабли
21.10.2026: миграция, которая затёрла прод
Это была не ошибка экспорта, а ошибка доверия к скрипту. Старый migrate-to-prod.sh скопировал базу с dev на prod. В теории — «синхронизация». На практике — потеря контента: репорты №11–13, проекты InINet/InINetWin, участники Flexin и Aevornyx.
Прод восстановили из архива /tmp/prod-before-migrate-20261021-0927.tar.gz, а dev-базу потом синхронизировали. Но осадок остался.
После этого появилось правило: перенос кода и перенос контента — разные операции. По умолчанию migrate-to-prod.sh копирует только код. Для базы нужен явный флаг --with-db, и перед ним печатается сравнение: счётчики, id, которых нет в другой базе. Для контента — --with-content.
Урок: скрипт, который «просто синхронизирует», опаснее скрипта, который требует подтверждения.
Бэкап не удаляет файлы
Ещё один призрак — откат. tar восстанавливает то, что было в архиве, но не удаляет файлы, созданные после. Если после бэкапа появился новый js/pjax.js, после отката он останется. И сайт будет вести себя странно: код старый, а кусок нового остался.
Поэтому после отката нужно руками смотреть, что появилось лишнего. Особенно если откат делается не в чистый каталог.
Кэш JSON
Даже если экспорт сработал, браузер может держать старый файл. Поэтому публичные запросы идут с ?t=Date.now() и заголовком no-cache. Это некрасиво, но работает.
Почему не API
Иногда спрашивают: «А почему не сделать нормальный API?»
Можно. Но для текущего масштаба API добавил бы:
- ещё один слой, который нужно держать живым;
- авторизацию для публичных чтений;
- CORS;
- кэширование;
- обработку ошибок;
- ещё одну причину, почему страница пустая.
JSON-файлы дают примерно то же самое, но их нельзя «уронить» запросом и они нормально кэшируются. Для витрины, которая меняется несколько раз в день, этого хватает.
API понадобится, когда:
- появится мобильное приложение;
- данные будут меняться в реальном времени;
- понадобятся сложные фильтры на сервере;
- несколько клиентов будут писать одновременно.
Пока этого нет — JSON честнее.
Правила, которые спасают
У меня есть небольшой чек-лист. Он скучный, но именно он держит схему живой:
- Любое изменение в админке заканчивается экспортом. Не «потом», не «в пятницу».
- JSON не редактируется руками. Только через базу.
- Перед переносом — сравнение баз. Не «на глаз».
- Контент живёт на проде. Dev — для кода. Если нужно перенести контент — только осознанно.
- После экспорта — проверить сайт с чистым кэшем. И с грязным тоже.
- Бэкап без восстановления — не бэкап. Хотя бы раз в месяц проверить, что архив разворачивается.
- Новый публичный файл — в sitemap или в исключения. Иначе он либо потеряется, либо попадёт туда, где не нужен.
Когда схема не подходит
Я не пытаюсь продать «SQLite + JSON» как универсальную архитектуру. Она не подходит, если:
- пользователи пишут одновременно и нужны транзакции между сущностями;
- данных столько, что JSON становится больше разумного;
- нужен поиск по полнотексту на сервере;
- несколько сервисов должны читать одни и те же данные;
- без API уже нельзя.
Для портала команды, где контент меняется вручную и читателей немного, схема работает. Она предсказуемая. Я знаю, где лежит каждая запись и что произойдёт после нажатия кнопки.
Вместо вывода
«Два мира» — это не бесплатно. За простоту публичной части платишь дисциплиной в админке.
Если бы я начинал заново, я бы всё равно выбрал SQLite и JSON. Но сразу сделал бы три вещи: сравнение баз перед миграцией, обязательный экспорт после изменений и проверку восстановления бэкапа. Тогда призраков было бы меньше.
Сайт — это не только страницы. Это ещё и путь данных. И если этот путь не описан, рано или поздно данные пойдут не туда.
