Это не вторая часть хронологии. Первая статья была про то, что происходило. Эта — про то, что я об этом думаю: где переоценил силы, что сломал и что бы сделал иначе.
С чего всё началось
Я хотел одну страницу. Не портал, не платформу, не «экосистему». Просто место, куда можно скинуть ссылку: кто мы, что делаем, какие у нас проекты.
Первая версия такой и была. Потом появились профили, админка, база, блог, статьи, Telegram, 2FA, сессии, репорты, темы, музыка, дорожная карта. Каждая фича была маленькой. Вместе они превратили страницу в систему, которую нужно обслуживать.
Это главный урок: сложность не приходит большими кусками. Она приходит по одной маленькой фиче.
«Одна страница» — самая дорогая фраза
Когда я добавлял очередной раздел, я думал: «это же просто». Профили — просто. Блог — просто. Статьи — просто. Кнопка репорта — просто.
Но «просто» складывается. Сначала появляется поле в базе. Потом — форма в админке. Потом — экспорт в JSON. Потом — страница на сайте. Потом — кэш, который надо сбросить. Потом — проверка прав, потому что не все должны это видеть. Потом — бэкап, потому что это уже данные.
Одна фича — это не одна кнопка. Это набор решений, которые потом живут вместе.
Если бы я начинал заново, я бы сразу завёл бюджет сложности: список того, что сайт делает, и список того, что он сознательно не делает. Не «когда-нибудь потом», а «нет, в этой версии этого не будет». Потому что «потом» не наступает — наступает следующая фича.
Я строил сайт, а получил инфраструктуру
Сайт давно не про страницы. Он про то, что за ними.
SQLite и экспорт в JSON, админка и роли, бэкапы и восстановление, dev и production, sitemap и SEO, cookie и приватность, сессии и 2FA, репорты и аудит. Этого не видно на главной, но без этого сайт разваливается.
Самый неприятный момент был, когда я понял: сайт — это не файлы, а процесс. Файлы можно перезаписать. Процесс нужно поддерживать: обновлять PHP, следить за сокетами, чистить кэш, проверять бэкапы, читать логи.
Красивая главная — это 10% работы. Остальные 90% — чтобы она открывалась.
Ошибки, за которые до сих пор неловко
Я не буду делать вид, что всё шло по плану. Вот несколько историй, которые лучше любого списка «best practices».
Бэкап, который унёс секреты
Одно время в архивах сайта лежал .env — файл с ключами Telegram и другими секретами. Не потому, что я хотел его туда положить, а потому, что бэкап делал «всё, что есть».
Потом я убрал .env из бэкапов и поставил права 600. Но старые архивы никуда не делись. Если такой репозиторий когда-нибудь утечёт, ключи придётся ротировать.
Урок: бэкап — это копия не только данных, но и поверхности атаки. Секреты должны жить отдельно от архивов.
Миграция, которая затёрла прод
Однажды скрипт переноса взял и скопировал базу с dev на prod. Не код — данные. За пару секунд на проде исчезли несколько репортов, проектов и участников, которых на dev не было.
Я восстановил всё из временного архива, который сделал перед запуском. Повезло: архив был. Но осадок остался.
Урок: код и контент надо переносить отдельно. Перед миграцией данных — сравнивать базы и спрашивать подтверждение. Скрипт, который умеет перезаписывать прод, должен быть страшнее, чем кажется.
Сокет, который умер после обновления
PHP обновился, PHP-FPM перезапустился, сокет /run/php/php8.3-fpm.sock протух. Caddy держал старый inode и отдавал 502 на всех PHP-страницах. Статика при этом работала — и это сбивало с толку: сайт вроде открывается, а админка и страницы проектов — нет.
Лечится docker restart caddy. Чтобы не повторялось, я добавил drop-in, который перезапускает Caddy после старта PHP-FPM.
Урок: «работает» — это не только код. Это ещё и сокеты, inode, контейнеры и порядок запуска. Автоматизация должна перезапускать не только то, что обновилось, но и то, что от него зависит.
Кэш, который показывал старое
Браузер держал старый page-init.js, тот — старый адрес футера, футер — старую навигацию. На главной всё было нормально, на внутренних страницах — нет. Playwright в чистом контексте этого не показывал: у него не было кэша.
Пришлось бампать версии у компонентов и у самих JS. Скучно, но работает.
Урок: кэш — это фича, но и источник багов. Версионировать надо всё, что может закэшироваться, и проверять на «грязном» профиле.
Файл, который молча не загрузился
Музыкальные треки не загружались. Не ошибка, не сообщение — просто ничего. Оказалось, upload_max_filesize по умолчанию 2 МБ, а треки больше. PHP молча отбрасывал файл.
Поднял лимиты до 24/32 МБ.
Урок: дефолты — это не требования. Если пользователь загружает файл, он должен узнать, что файл не принят. Молчание — худший интерфейс.
Тест, который написал в боевую базу
Я проверял обработчик через CLI с подставленным $_POST. Обработчик честно сохранил данные. В боевую базу. И затёр README одного из проектов.
Восстановил из бэкапа. После этого правило простое: тесты не должны трогать реальные данные. Либо отдельная база, либо снапшот полей до и после.
Урок: «я только посмотреть» — опасная фраза, когда в руках боевая база.
rsync --delete и два файла
Скрипт переноса на прод синхронизировал директории с --delete. Логично: на проде не должно быть лишнего. Но на проде были файлы, которых нет на dev: тизер и специальная 404 для поддомена. Они удалились.
Пришлось добавлять исключения.
Урок: деструктивная синхронизация должна знать, что именно должно выжить. Идеально — dry-run и список исключений, а не «на глаз».
Логин и ник, которые были одним полем
Сначала логин и публичный ник были одним и тем же. Потом понадобилось разделить: логин — приватный, для входа; ник — публичный, для профиля и URL.
Пришлось мигрировать, объяснять, что старый логин может не работать, и перепроверять вход. Зато теперь можно сменить ник, не ломая вход, и наоборот.
Урок: публичные и приватные идентификаторы — разные данные. Если их склеить, потом придётся распиливать.
Статьи: unlisted, которого не было
Я долго думал, что хочу «опубликовано, но скрыто»: статья доступна по ссылке, но не в списке. Как unlisted на YouTube.
Потом понял: мне это не нужно. Есть два понятных состояния: опубликовано — видно всем, и черновик — не видно никому, кроме автора и админа. Всё остальное — промежуточные статусы проверки.
В итоге: опубликованная статья — в списке, в sitemap и по прямой ссылке; черновик и материал на проверке — 404 и предпросмотр по токену; «Снять с публикации» возвращает в черновики. Без «полускрытых» статей.
Урок: если в модели статусов больше состояний, чем ты можешь объяснить одной фразой, — это не гибкость, а баг.
Безопасность — это не паранойя, а гигиена
Самые заметные фичи — темы, анимации, музыка. Самые важные — те, которых не видно: 2FA, реестр сессий, уведомления о входе, лимиты, CSP, права, бэкапы без секретов.
Я не security-инженер. Но я понял простую вещь: безопасность — это не «сделать один раз». Это привычка: не хранить секреты в коде, не логировать пароли, проверять права на каждом действии, не доверять пользовательскому вводу.
Впереди ещё TOTP, аккуратный lockout, SRI и проверка внешних запросов. Но база уже есть.
Урок: скучные функции — это не «потом». Именно они решают, можно ли доверять сайту.
Дизайн: атмосфера не должна спорить с текстом
Сайт должен выглядеть как место, где делают код и игры: чёрный, красный, терминал, матрица, курсоры. Но если эффекты мешают читать — они проиграли.
Я стараюсь держать баланс: атмосфера на первом экране, спокойный текст в статье, темы для тех, кому не нравится красный, возможность отключить лишнее.
Урок: стиль — это инструмент, а не клетка. Если читатель не может прочитать текст, красивый фон уже не помогает.
Люди и контент: самое сложное — не код
Код можно переписать. Контент и людей — сложнее.
Профили, роли, модерация, статьи, бейджи, репорты — всё это про доверие. Кто может публиковать? Кто проверяет? Что видит гость? Что видит участник? Где заканчивается «моё» и начинается «общее»?
Я долго не открывал статьи, потому что не хотел пустую витрину. В итоге мы открыли раздел, когда появились тексты. Лучше несколько нормальных статей, чем раздел с заголовком «скоро».
Урок: портал без контента — это каркас. Контент — это продукт, а не украшение.
Бот для участников, канал для всех
Отдельная история — Telegram.
Канал — публичный: новости, анонсы, ссылки на статьи. Его можно читать всем.
Бот — внутренний инструмент участников: уведомления о входе, 2FA, сессии, события. Посетителю он не нужен, и предлагать его вместо обратной связи — неправильно.
Для идей и проблем у гостя есть канал, почта и кнопка «Сообщить о проблеме» на сайте. Бот — не для этого.
Я сам поймал эту несостыковку в тексте статьи: написал «Telegram-бот и почта», хотя бот — для своих. Исправил. Иногда проще заметить ошибку в тексте, чем в коде.
Урок: не рекламируй внутренние инструменты наружу. У каждого канала связи должна быть понятная аудитория.
Что бы я сделал иначе
Если бы можно было вернуться в начало, я бы:
- Сначала описал данные и экспорт, а уже потом рисовал страницы.
- Сразу сделал бэкапы и проверил восстановление, а не «потом».
- Держал dev и prod по-настоящему раздельно: код отдельно, контент отдельно.
- Записывал решения и грабли в один файл с первого дня, а не восстанавливал по памяти.
- Проверял на чистом браузере и на «грязном» кэше — оба сценария важны.
- Добавил базовую безопасность раньше: сессии, права, секреты, 2FA.
- Говорил «нет» новым фичам, пока текущая не закончена.
- Писал тексты раньше: они объясняют проект лучше, чем changelog.
Что дальше
Портфолио для креаторов, TOTP, публичный API, больше статей и проектов. Но главное — не превратить сайт в музей.
Я хочу, чтобы он оставался живым: с новыми людьми, текстами, странными идеями и нормальной обратной связью.
Вместо вывода
Спасибо, что зашёл.
Если есть идея — напиши в Telegram-канал, на почту или через кнопку «Сообщить о проблеме» внизу. Бот оставим участникам: у них свои задачи.
Это только начало.
