// статьи / статья
#статья #сайт#iniproject#разработка#ошибки#выводы

> Что я понял, пока делал InIProject: сомнения, ошибки и выводы

Честный разбор без хронологии: где я переоценил силы, что сломал и что бы сделал иначе.

SkilinPur 25.10.2026 8 мин чтения серия: как мы делали 👁
Что я понял, пока делал InIProject: сомнения, ошибки и выводы

Это не вторая часть хронологии. Первая статья была про то, что происходило. Эта — про то, что я об этом думаю: где переоценил силы, что сломал и что бы сделал иначе.

С чего всё началось

Я хотел одну страницу. Не портал, не платформу, не «экосистему». Просто место, куда можно скинуть ссылку: кто мы, что делаем, какие у нас проекты.

Первая версия такой и была. Потом появились профили, админка, база, блог, статьи, 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-канал, на почту или через кнопку «Сообщить о проблеме» внизу. Бот оставим участникам: у них свои задачи.

Это только начало.

// реакции
SkilinPurучастник InIProject