108 окон, 0 фреймворков, 1 Kubernetes: как команда без разработчиков построила интерактивный дом-таймлайн про ТВ 90-х

от автора

Привет, Хабр. Это история о том, как мы собирались и по вечерам за полгода создали интерактивный веб-проект про российское телевидение 90-х и 00-х.

Сайт называется Окна.ТВ. Заходите, скролльте и вспомните время, когда телевизор был нашим главным развлечением.

Главный экран сайта — вид дома с окнами, таймлайн справа, десктоп

Главный экран сайта — вид дома с окнами, таймлайн справа, десктоп

Стек в двух словах: ванильный JS, собственная CMS, собственный видеоплеер, деплой в Kubernetes. По дороге познакомился с S3, OOM killer, mask-border и Safari — с последним лучше бы не знакомился ))


Команда

Наша команда работает в Городских Медиа Шкулев Холдинга. Я – руководитель отдела дизайна, Полина и Маша – дизайнеры, Илья – наш арт-директор. В конце лета 2025 года Илья предложил сделать что-нибудь крутое и масштабное, выходящие за рамки нашей рабочей рутины. Просто потому что мы можем. Идея создать то, чем можно гордиться и не быть зависимым от дедлайнов и заказчиков, всем очень понравилась.

Илья Давыдов — арт-директор. Инициатор проекта, именно он предложил тему телевидения. Организовывал процессы, занимался административкой, давал фидбек по каждому элементу, предлагал и согласовывал все наши решения. Помогал с визуалом, занимался контентом, выступил в качестве главного редактора, вычитав все тексты на сайте.

Я (Виталий Калистратов) — руководитель отдела дизайна, но в этом проекте занимался всей технической частью. Фронтенд, бэкенд, CMS-админка — всё написано мной с помощью нейросетей. Придумал концепцию сайта с инвертированным скроллом и таймлайном. Здесь впервые нужно было сделать что-то по-настоящему красивое, работающее на всех устройствах и во всех браузерах.

Полина Авдошина — дизайнер. Отвечала за красоту каждого окошка и каждого модального окна. Генерировала интерьеры квартир через нейросети: ImageFX, Flux, Nanabanana Наполняла дом через админку. Она же придумывала все пасхалки и детали: спутниковые тарелки, кондиционеры, кошки на подоконниках.

Маша Романова — тоже дизайнер, но в этом проекте ей пришлось стать нашим автором. Львиная доля текстов на сайте написано ей. Маша изучала каждую программу, копала архивы, искала интересные факты – в общем старалась сделать каждый текст уникальным, теплым и небанальным.

Саша Соколова — видеоредактор, в нашем холдинге занимается обработкой всего видеоконтента. Саша присоединилась к нам по ходу проекта, так изначально мы переоценили свои силы: проект разросся и в общей сложности нам необходимо было написать 141 материал! Да у нас не было сроков, но тянуть вечность мы тоже не могли. Саша нас выручила и стала вторым автором, к тому же за ее плечами журналистское образование.

Мы не профессиональная студия разработки. Мы дизайнеры и редакторы, которые захотели сделать что-то крутое. И сделали. При этом основная работа никуда не делась, проект жил параллельно с повседневными задачами. Без дедлайнов, без давления, на чистом энтузиазме.


Почему телевидение

В конце лета 2025 года Илья предложил сделать что-нибудь масштабное — выходящее за рамки рабочей рутины. Просто потому что можем. Идея создать что-то, чем можно гордиться и не быть зависимым от дедлайнов и заказчиков, всем очень понравилась.

Мы долго выбирали направление: музыка, фильмы, исторические личности, архитектура… В какой-то момент, мы думали посвятить наш проект проблемам и взаимоотношениям между разными поколениями: бумеры против зумеров. Но потом Илья предложил тему, которая зацепила всех – телевизор. Почему бы не рассказать о том, как развивалось отечественное телевидение, ведь оно во многом стало важным фактором в воспитании нас и нашего поколения? Мы сразу вспомнили свои любимые программы из детства и поняли, что они четко характеризуют нас самих! С этого момента началась наша работа – джунгли зовут!

От Figma к живому прототипу

Первая раскладка была в Figma: дом, окна, таймлайн, модалки. Выглядело красиво, но Figma — это статика. По ней непонятно, как оно будет жить, ощущается ли скролл, как будет работать таймлайн.

Прототип в Figma — раскладка дома с окнами

Прототип в Figma — раскладка дома с окнами

Тут я вспомнил проект Borjomi Самый глубокий сайт в мире, где нужно скроллить вниз — всё глубже к скважине, сбоку бежит шкала глубины. И подумал: а что если инвертировать? Пользователь начинает внизу и скроллит вверх, как будто поднимает взгляд на дом. Внизу девяностые, наверху десятые. Справа таймлайн.

Сел и за час вместе с Gemini 2.5 собрал HTML-прототип на 400 строк. Там уже было всё главное: окна, модалки, инвертированный скролл, таймлайн с fish-eye эффектом. Показал команде — все сказали «ок, погнали». Этот первый прототип прикреплю в комменты в Telegram, если хотите посмотреть с чего всё начиналось, заглядывайте.

Живой прототип DOM.html

Живой прототип DOM.html

Вся магия «дом идёт снизу вверх» сводится к двум CSS-строкам: flex-direction: column-reverse на обёртке и grid-template-columns: repeat(3, 1fr) для трёх окон в ряд. Большинство решений из того прототипа дожили до продакшна почти без изменений.


Почему ванилла и без фреймворков

Сразу оговорюсь: я вайбкодер. Я не учил языки программирования. За пару лет работы с нейронками я научился читать код, чувствую, когда нейронка идёт не туда и уверенно несет херню, но писать с нуля сам не могу.

Поэтому принцип «никаких фреймворков, никаких лишних зависимостей» был не идеологическим, а практическим: чем проще стек, тем лучше. Vanilla JS, чистый CSS, обычный HTML, Node.js + Express на бэке. Начинал с Gemini 2.5, потом Gemini 3 Pro, в конце перешёл на Claude Code с Opus 4.6. С каждой моделью качество и чистота кода росли.

Бонусом это хорошо легло на оптимизацию. Без React и прочего бандл — это просто ES6-модули, которые браузер подгружает напрямую. Никакого гидрейтинга, никакого рантайм-оверхеда.


Бэкенд: от JSON-файла на VPS за $5 до Kubernetes

Первая версия крутилась на моём личном VPS в Чикаго. Данные хранились в data.json. Node.js + Express + один файл с состоянием этого хватало для прототипа.

Бэкенд вообще возник из простой необходимости: нужна была CMS. Я физически не мог добавить все 108 окон вручную через код, а команда должна была работать параллельно.

Визуальный редактор дома в админке — кликаешь на окно, редактируешь содержимое прямо в сетке

Визуальный редактор дома в админке — кликаешь на окно, редактируешь содержимое прямо в сетке

Когда мы показали проект корпоративной команде разработки, меня пустили в инфраструктуру холдинга и тут начался новый этап. Я узнал, что такое S3 и как с ним живёт MinIO, чем поды отличаются от неймспейсов в Kubernetes, из каких стадий состоит CI/CD-пайплайн в GitLab. Переезд занял несколько дней: другая архитектура хранения файлов, MariaDB вместо JSON, корпоративный VPN, специфика Nginx как reverse-proxy.

За эту неделю я узнал столько, сколько не узнал бы за год, пиля ботиков на личной VPS. Для меня это главный лут проекта.


Картинки: из 35 секунд в 2,5

Сайт графически нагруженный: текстуры дома, интерьеры 108 окон, муралы. Изначально всё лежало в PNG и грузилось в среднем 35 секунд.

Прикрутил автоматическую конвертацию при загрузке через админку.

PNG 4,7 МБ превращается в WebP 152 КБ — сжатие в 31 раз.
PNG 1600 КБ в AVIF 45 КБ — в 35 раз.

Полина загружает PNG бэкенд сам делает WebP, AVIF и blur-up миниатюру. Никто из команды не думает про форматы, про вес, про оптимизацию.

В CSS используется image-set() с фоллбэками, браузер сам выбирает поддерживаемый формат:

// Пользователь начинает внизу: после загрузки скроллим страницу в самый низwindow.scrollTo(0, document.body.scrollHeight);// Fish-eye на таймлайне: чем ближе год к центру экрана, тем он крупнееconst distanceFromCenter = Math.abs(timelineCenterY - elementCenterY);if (distanceFromCenter < fishEyeRadius) {    const ratio = 1 - (distanceFromCenter / fishEyeRadius);    const scale = 1.0 + (maxScale - 1.0) * ratio;    element.style.transform = `scale(${scale})`;}

С Sharp и AVIF пришлось немного помучиться. При загрузке больших исходников сервер временами падал: 502, 504, снова 502. Выяснилось, что Sharp при конвертации в AVIF загружает исходник в RAM и держит буфер — при параллельных загрузках на контейнере с лимитом памяти OOM Killer прибивал Node.js и PM2 его радостно перезапускал.

Фикс в итоге был двойным: конвертация идёт стримами вместо буферов (меньше памяти одновременно), а для AVIF параметр effort снижен до 2 — это значительно уменьшает потребление RAM при кодировании. Плюс поставил лимит: максимум две обработки изображений одновременно через простую очередь. Стабильно.

Видео тоже жмём: WebM-файлы на S3, средний вес ~3 МБ на ролик.


Прелоадер в стиле ТВ-помех

Прелоадер — ТВ-помехи на Canvas, пока грузятся все ассеты

Прелоадер — ТВ-помехи на Canvas, пока грузятся все ассеты

Раз проект про телевидение, прелоадер должен быть тематическим. Главное условие — когда сайт наконец появляется, он должен выглядеть сразу идеально, без постепенной подгрузки текстур. Если дом будет проявляться по кусочкам, вся магия рассыпается.

Реализовал ТВ-помехи прямо на Canvas через requestAnimationFrame: белый шум, «поиск каналов» с бегущей частотой в мегагерцах и счётчиком найденных каналов — всё как на старом телевизоре.

Почему Canvas, а не WebM? Видео это зацикленные кадры, и глаз замечает петлю за пару секунд, иллюзия ломается. Canvas генерирует новый кадр каждый раз через Math.random(), и шум никогда не повторяется. Плюс шум ужасно жмётся кодеками, в нём нет паттернов. Полноэкранный WebM весил бы в разы больше, чем 310 строк JS.

Параллельно с прелоадером в фоне тянутся самые тяжёлые ассеты. Когда загрузка закончилась, Canvas гасится, дом появляется уже целым.

Небольшая пасхалка в виде помех при наведении на заголовок

Небольшая пасхалка в виде помех при наведении на заголовок

Бонусом тот же модуль отдаёт функцию generateHeadlineNoise(), которая генерирует четыре статичных кадра шума и записывает их в CSS-переменные. Заголовок в самом низу сайта при наведении начинает рябить этими же помехами, просто CSS-анимация переключает четыре фона. Шумогенератор один, применений два.


Blur-up и умный preload

На сайте 108 модалок, у каждой своё уникальное изображение интерьера в высоком разрешении (спасибо Полине). Тянуть их все при загрузке значит убить трафик пользователю, который, скорее всего, не будет открывать каждое окно. Но и просто показывать спиннер на полсекунды это минус для атмосферы.

Компромиссом стало blur-up превью. Когда Полина загружает картинку в админку, Sharp попутно генерирует крошечную миниатюру шириной 50 пикселей — буквально пара килобайт. Эта миниатюра приходит вместе с данными сайта. Кликаешь на окно: сразу показывается размытая комната в правильных цветах, через секунду размытие плавно растворяется в детализированную картинку.

Те же ТВ-помехи из прелоадера живут и внутри модалок — пока видео тянется с S3, телевизор «ищет канал».

Те же ТВ-помехи из прелоадера живут и внутри модалок — пока видео тянется с S3, телевизор «ищет канал».

Чтобы к моменту клика картинка уже была в кэше, поставил preload по ховеру на окно но с задержкой в 200 мс. Сначала я делал наивно: mouseenter → грузим картинку. Работает, все счастливы. Но когда курсор пролетает над десятком окон за секунду, просто разглядывая дом. Каждый такой пролёт дёргал загрузку, в сумме утекали мегабайты на картинки, которые никто не собирался смотреть.

const onMouseEnter = () => {    hoverTimer = setTimeout(preloadImage, 200);};const onMouseLeave = () => {    clearTimeout(hoverTimer); // ушёл раньше 200 мс — отменяем};

Эти 200 мс отсекают транзитные проходы. На мобиле ховера нет, там IntersectionObserver: пока окно на экране, его превью тихо подгружается в фоне.


Дом не должен быть ровным прямоугольником

По умолчанию grid рисует идеальный прямоугольник. У настоящего дома края неровные: кирпичи торчат, штукатурка отваливается, силуэт живой. Нарисовал PNG-маску с неровными краями стены и наложил её через CSS-свойство mask-border. Дом сразу стал чуть живее и на десктопе эта мелочь создаёт ту самую атмосферу.

Слева — PNG-маска с неровными краями стены, справа — как она ложится на дом через mask-border

Слева — PNG-маска с неровными краями стены, справа — как она ложится на дом через mask-border

На мобилке пришлось маску отключить, потому что она провоцировала мерцание муралов при скролле. Safari, привет.


Параллакс и fish-eye таймлайн

При скролле окна дома и их интерьер смещаются с разной скоростью, создавая ощущение глубины. Амплитуда всего 25 пикселей, эффект тонкий, но без него скролл был бы плоским.

Сначала дёргал getBoundingClientRect() в скролл-хандлере для каждого окна. При сотне окон это превращается в слайдшоу на старых телефонах. Решение — кэшировать позиции один раз при инициализации и обновлять только при ресайзе. На каждом кадре работать с кэшем и применять translateY только к видимым окнам.

Вертикальная шкала справа от дома — fish-eye таймлайн, синхронизированный со скроллом. Годы ближе к центру экрана визуально увеличиваются:

location /uploads/ {    proxy_pass http://minio-endpoint/bucket/okna-ru/uploads/;    client_max_body_size 50M;}

Та же история с кэшированием — без него таймлайн ощутимо тормозил на 40+ годах.


Модальные окна: deep linking, навигация, свайпы

Клик по окну открывает модалку с передачей. Несколько деталей, которые хочется упоминуть.

Deep linking. Slug передачи пишется в hash URL — #zov-dzhunglej. При загрузке страницы я проверяю hash и сразу открываю нужное окно. Можно кинуть ссылку на конкретную передачу в семейный чат, и она откроется именно там. Slug генерируется автоматически из названия через самописную таблицу транслитерации кириллицы — 33 буквы + спецсимволы, никаких внешних библиотек. «Зов джунглей» → zov-dzhunglej.

Навигация. Не закрывая окно, можно листать соседние — стрелками на клаве, кликом по бокам на десктопе, свайпом на мобильном. Анимация slide + fade, 150 мс.

Мелочи. Вместо скучного «Поделиться» при каждом открытии модалки всплывает случайная фраза из пятнадцати вариантов — «Перешли маме, она оценит», «Скинь тому, кто поймёт без слов». На функциональность не влияет, но опять очередной + к атмосфере проекта.


Свой видеоплеер

Изначально использовали VK-плеер. У него два неприятных момента: реклама и логотип VK. Пользователь заглядывает в окно 1993 года, видит советский телевизор, нажимает play — и вылезает реклама маркетплейса 2026 года. Вся атмосфера ломается.

Написал свой минималистичный плеер: WebM-файлы на S3, кастомный UI поверх <video>. Play/pause, mute/unmute, прогресс-бар с seek, полноэкранный режим. 177 строк JS, 281 строка CSS. Никакой рекламы, никаких логотипов.

Плеер живёт в двух местах. Первое — основное видео в модалке окна-передачи. Пока видео тянется с S3, на его месте играют те же ТВ-помехи из прелоадера с частотомером и счётчиком каналов. Видео готово — помехи гасятся, канал «пойман».

Мурал в 3 этажа, при клике, открывается длинная статья про телекомпанию или явление эпохи

Мурал в 3 этажа, при клике, открывается длинная статья про телекомпанию или явление эпохи

Второе место — inline-плеер внутри мурал-модалок. Мурал — это большой горизонтальный блок в 3 этажа, при клике открывается длинная статья про явление: телекомпанию, личность, жанр. В этих статьях через WYSIWYG-редактор можно вставить видео в двух режимах. Gif-режим — зацикленный ролик без звука, весит в разы меньше настоящего GIF и не душит CPU. Режим с плеером — полноценный плеер с управлением. При старте любого плеера остальные в той же модалке автоматически ставятся на паузу.


Онбординг, который молчит пока не нужен

Проект нестандартный, и пользователь не сразу понимает, что с окнами можно взаимодействовать.

Логика в onboarding.js такая: если пользователь уже кликал окно, ничего не показываем — флаг в localStorage. Если нет — ждём 7 секунд. Если всё это время он скроллит без остановок, ждём паузу в 2 секунды, когда он явно что-то рассматривает. Тогда всплывает мягкая подсказка «кликни на окно». При первом клике она убирается навсегда.

Онбординг, который не мешает и появляется только когда пользователь реально запутался. Такую логику я люблю.

Онбординг, который не мешает и появляется только когда пользователь реально запутался. Такую логику я люблю.

Мобильная версия — отдельный проект

Мобильная версия: два окна на этаже, видео в модалках сверху, текст снизу, свайп вместо стрелок

Мобильная версия: два окна на этаже, видео в модалках сверху, текст снизу, свайп вместо стрелок

Два окна на этаже вместо трёх, другой формат муралов, модалки полностью перестроены: видео сверху, текст снизу, свайп вместо стрелок. В responsive.css 524 строки — и это после чистки.


Система этапов

108 окон — это много. Чтобы не вываливать всё разом и иметь повод вернуться, мы разбили контент на 4 этапа. С каждым следующим этапом открывались новые этажи дома.

Оверлей «Не переключайтесь» на последнем murале этапа — завтра, 20 апреля, выходит заключительный

Оверлей «Не переключайтесь» на последнем murале этапа — завтра, 20 апреля, выходит заключительный

Технически этапы — просто поля в БД: stage на окне и currentStage в глобальных настройках. На фронте окна с stage > currentStage либо скрыты, либо показаны как «закрытые». На последнем murале каждого этапа висит оверлей «Не переключайтесь» с призывом подписаться на Telegram. Текст, подзаголовок и кнопка редактируются из админки без единой строки кода.


Админка: проект внутри проекта

Если кажется, что фронт со 108 окнами — большой проект, то админка получилась в три раза больше. Без неё я бы захлебнулся: каждый член команды должен был уметь вносить изменения сам, а не через «Виталь, добавь пожалуйста».

Drag & drop в админке — решили что «Ералаш» должен быть выше «Зова джунглей», перетащили, сохранили

Drag & drop в админке — решили что «Ералаш» должен быть выше «Зова джунглей», перетащили, сохранили

Получилась полноценная CMS. Видишь весь дом целиком, кликаешь на окно, редактируешь содержимое. Есть переключатель «десктоп/мобайл», чтобы сразу видеть как дом выглядит на телефоне. Окна перетаскиваются мышкой — решили, что «Ералаш» должен быть выше «Зова джунглей», перетащили, сохранили. WYSIWYG-редактор на Pell — дважды пробовал заменить на Quill, дважды откатывал: новые баги плодились быстрее, чем исчезали старые. «Работает — не трогай» победило.

Автоконвертация при загрузке: загружаешь PNG, получаешь WebP, AVIF и blur-up миниатюру автоматически. Команде не нужно думать про форматы.

Настройки сайта — цвета фона по этапам, тексты первого экрана и подвала, оверлей, кнопки соцсетей, команда проекта с ролями — всё редактируется без кода.


Коллаборативные блокировки без Redis и WebSocket

Если Полина и Маша одновременно откроют одно и то же окно и начнут редактировать, последняя, кто нажмёт «Сохранить», затрёт работу второй. Классическая гонка.

Нормальное решение — Redis + WebSocket с реалтайм-пушами. Но для команды из четырёх редакторов это паровой молот для забивания гвоздика.

Тут сделал проще. Три API-эндпоинта: взять блокировку, отпустить, получить статус всех. In-memory Map, где ключ — ID окна, значение — имя редактора и время захвата. Есть TTL: если блокировка не обновлялась больше N минут, она протухает и снимается автоматически — редактор мог закрыть вкладку, не нажав «сохранить».

Коллаборативные блокировки в работе, остальные видят замок и заблокированную кнопку

Коллаборативные блокировки в работе, остальные видят замок и заблокированную кнопку

Каждая копия админки поллит статус раз в несколько секунд. Видишь в списке, что окно занято Полиной, значок замка и кнопка редактирования серая. Полина закрыла окно → блокировка упала → через секунду замок у всех пропал.


SEO и аналитика

Стандартный набор OpenGraph в <head> — когда кидаешь ссылку в Telegram или VK, там красиво разворачивается превью с домом. Кажется мелочью, но для контентного проекта заметно бьёт по кликам. admin.html помечен как noindex, чтобы Google не индексировал страницу логина CMS.

Яндекс.Метрика подключена, но с важной особенностью. Стандартно Метрика считает page view по загрузкам страниц. У нас SPA с хэш-навигацией — для Метрики это одна страница, и мы теряем ключевую метрику «какие передачи реально смотрят». Фикс — виртуальные просмотры: на каждое открытие модалки вызываем ym(id, 'hit', '/window/zov-dzhunglej', { title }). Теперь в Метрике видно — «Поле чудес» 2400 открытий, «Зов джунглей» 1800. Для нас это бесценно: понятно, какой контент лучше почитали и какое окно чаще открывали.


Рефакторинг: когда костыли стали небоскрёбом

Когда начинал, у меня не было чёткой картины, во что это выльется. Первый прототип был одним HTML-файлом. Через месяц это был один script.js на 2275 строк. Через два — style.css на 2639 строк. Нужен плеер вместо VK? Ещё 300 строк в script.js. Система этапов? Новые функции туда же. Код работал, но найти в нём что-то стало очень сложно.

К февралю стало очевидно: рефакторинг необходим. С Claude Code за три дня разложил всё по полочкам.

Один script.js стал 22 ES6-модулями — каждый отвечает за свою область: модалки, таймлайн, параллакс, видеоплеер, аналитика, онбординг. style.css разбился на 10 файлов по компонентам. Бэкенд аналогично: server.js теперь это около сотни строк точки входа, вся логика разнесена по routes/, services/, middleware/, utils/.

После рефакторинга проект стало приятно поддерживать. Самое важное — стало легко объяснить структуру новой сессии нейросети: «вот CLAUDE.md, в нём описана архитектура, давай разбираться».


Что получилось неожиданно: это конструктор

Я строил конкретный сайт про ТВ. А получилась платформа.

В какой-то момент заметил, что каждый раз, когда кто-то из команды спрашивал «а можно сделать вот это?», я просто добавлял это в админку как настройку. Постепенно всё, что можно было вынести в конфиг, было вынесено: количество этажей и размер сетки, цвета по этапам, тексты, пасхалки как опциональные слои.

Настройки сайта в админке — этапы, цвета фона, промо-тексты. Всё что видит пользователь меняется без редактирования кода

Настройки сайта в админке — этапы, цвета фона, промо-тексты. Всё что видит пользователь меняется без редактирования кода

Сегодня из этой CMS можно собрать совсем другой сайт с домом на любую тему, не тронув ни строчки кода. Хочешь «дом» про историю кино? Меняешь текстуры, годы, загружаешь другие интерьеры — и всё. Про классические альбомы, про эпохи видеоигр, про архитектурные стили Москвы — это просто другой контент в той же CMS.

Что с этим делать дальше — пока незнаем. Возможно, это тема для следующей статьи. Но то, что «хотим сделать что-то крутое» неожиданно превратилось в рабочий инструмент это наверное, самый странный результат всей этой истории.


Нейросети и что я вынес

Этот проект не состоялся бы без нейронок. Это факт, а не комплимент. Полина генерировала интерьеры с помощью них. Я писал весь код. С каждой моделью решения становились элегантнее и чище.

Четыре года назад дизайнер не мог просто взять и написать полноценное веб-приложение с бэкендом, CMS и деплоем в K8s. Сегодня может. Не нужно тратить годы на изучение языков, чтобы реализовать идею. Нужно уметь формулировать задачу, проверять результат и чувствовать, когда нейронка несёт ерунду.

Как дизайнер я формально знал про WebP и AVIF, но впервые по-настоящему разобрался в их внутренностях: настройки Sharp, когда стоит параллельная обработка, когда нет. Впервые прикоснулся к S3, Kubernetes, CI/CD-пайплайны, MariaDB. Разобрался с IntersectionObserver, с разницей между requestAnimationFrame и setTimeout(fn, 16), с image-set(), mask-border и жизнью в column-reverse.

Я первый раз в жизни сверстал сайт, который красиво выглядит и на 27-дюймовом мониторе, и на iPhone SE. Написал бэкенд с авторизацией, rate limiting и коллаборативными блокировками. Сделал CMS, которой параллельно пользуются четыре человека и она не развалилась.

Вёрстка сайтов это, конечно, отдельный вид мазохизма. Но если кто-то откроет наш сайт и увидит «Поле чудес» или «Зов джунглей» и вспомнит, как сидел у бабушки перед кинескопом — значит, мучения были не впустую.


Сайт проекта: okna.tv
Telegram проекта: @tv_okna
Мой телеграм, где пишу про свои проекты: @VETA14
В комментариях положу тот самый первый прототип HTML с которого всё началось, буду рад подписке 🙂

Если есть вопросы по архитектуре, оптимизации изображений, Canvas-эффектам или просто по вайбкодингу — задавайте в комментариях, с удовольствием отвечу.

ссылка на оригинал статьи https://habr.com/ru/articles/1025152/