Как собран этот сайт: четыре отказа и один обход защиты
Разбор сборки roninsystems.dev — от прототипа до автономного приложения на Next.js и FastAPI. Интереснее стека оказалось то, что ломалось по дороге.
Ronin Systems долго существовал сразу в нескольких местах: Telegram, YouTube, Telegraph, торговые платформы, профили на фриланс-площадках. Сайт должен был собрать это в одну систему и развести два направления — Engineering и Systematic Trading.
Начали с визуального прототипа: он быстро проверил концепцию. После этого техническую основу собрали заново, чтобы сайт не зависел от конструктора и мог развиваться как обычное приложение.
Ниже — не список технологий, а разбор того, что сломалось по дороге. Это полезнее.
Что получилось
Next.js 16, React 19, TypeScript. Отдельный FastAPI на Python 3.12 для формы связи, PostgreSQL в Neon, Docker и деплой на Fly.io — два приложения во Франкфурте по 256 МБ. Статьи и кейсы в MDX без CMS, две локали с автоматической проверкой соответствия, три темы оформления, применяемые до первой отрисовки.
Все страницы предрендерены, клиентского JavaScript минимум.
Обход ограничения частоты запросов
Форма связи ограничена по числу запросов с одного адреса. Проверка выглядела так: семь запросов подряд, в каждом свой X-Forwarded-For. Лимит — пять. Прошли все семь.
Логика понятна: заголовок подделывается тривиально, значит доверять ему нельзя. Поправили источник доверенного адреса в приложении, перезапустили, повторили. Снова семь из семи.
Причина оказалась этажом выше. Uvicorn включает обработку proxy headers по умолчанию и доверяет X-Forwarded-For от любого узла — то есть переписывает request.client на уровне ASGI ещё до того, как отработает код приложения. Прикладная починка была верной и полностью бессмысленной: её обходили раньше, чем она успевала сработать.
Лечится двумя вещами: --no-proxy-headers при запуске и доверенный заголовок, который ставит собственный серверный прокси сайта — тот самый, через который единственно и можно достучаться до API. Плюс тест, который проверяет наличие флага в Dockerfile, чтобы его не убрали случайно.
Общий вывод шире конкретного фреймворка: прежде чем чинить доверие к данным, стоит выяснить, кто вообще их подставляет. Слой, о котором вы не подумали, может уже принять решение за вас.
Три отказа поменьше
Сайт падал на 256 МБ. Запрос картинки в 1920px убивал процесс через OOM-killer, health-check уходил в ноль. Дело оказалось не в размере машины: исходное изображение было 2816×1536 при том, что в разметке объявлено как 1408×768, вчетверо больше нужного. Sharp разворачивает картинку в память попиксельно перед сжатием. Лечится приведением исходника к заявленному размеру и ограничением ширин, которые вообще разрешено генерировать.
Переходы приземлялись в середине страницы. scroll-behavior: smooth на html применяется не только к якорям, но и к скроллу роутера наверх при навигации, а входящая страница эту анимацию прерывает. Проверили сравнением на одной странице: со smooth три перехода с позиции 2400 закончились на 2400, 2400 и 1970; с auto — все три на нуле. Плавность вернули точечно, на клик по оглавлению.
Переключатель языка вёл на 404. Английская версия отдаётся с корня и переписывается на /en внутри, поэтому роутер видит /en/engineering, а адресная строка — /engineering. Ссылка на другую локаль строилась из адреса и давала /ru/en/engineering. Лечится чтением пути из дерева маршрутов, а не из адресной строки.
У всех трёх общая черта: каждый работал корректно в изоляции и ломался на стыке — приложение и рантайм, CSS и роутер, роутер и прокси.
Два CSS, наложенных друг на друга
Исходный файл стилей был на 1075 строк, где вторая половина переопределяла первую: :root объявлен дважды, --max-width задан сначала одним значением, потом другим, медиа-запросы продублированы, 201 объявление шрифта и около тридцати разных clamp-кривых на одни только заголовки первого и второго уровня.
Внешне это выглядело как набор отдельных элементов, а не одна страница — каждый блок подгонялся вручную, потому что общей сетки не было.
Пересобрали вокруг слоя токенов: типографическая шкала, spacing, двенадцать колонок. Ни одно правило не задаёт размер шрифта напрямую. Цвета и визуальный язык при этом не менялись.
Что решили не делать
Redis не добавляли. Единственное состояние в памяти — счётчик ограничения частоты, и при одной работающей машине он корректен. Станет неверным при горизонтальном масштабировании — тогда меняется один класс с тем же интерфейсом.
CMS не подключали. Статьи и кейсы лежат в репозитории как MDX. Контент проходит те же проверки, что и код: скрипт падает, если у документа нет второй локали или структура заголовков разошлась.
Аналитику не ставили. Её просто нет — ни счётчиков, ни трекинговых cookie.
Что из этого следует
Корпоративный сайт обычно относят к маркетингу, а не к инженерии. Отсюда конструкторы, отсутствие тестов и невозможность что-либо проверить.
Но у него ровно те же свойства, что у любой системы: границы, состояние, доверие к внешним данным, поведение под нагрузкой. И ломается он так же — на стыках, а не внутри компонентов.
Разница между «сделали сайт» и «построили систему» видна не в день запуска, а через полгода, когда в неё нужно что-то добавить.