Пять неочевидных вещей, которые я узнал, запуская кино-соцсеть: от robots.txt-ловушки до 24-мерной математики вкуса

от автора

Последние полгода я работаю над VibeMuvik — кино-соцсетью с рецензиями, дебатами и синхронным просмотром фильмов. Одна из тех штук, которые «ну вроде несложно», пока не начинаешь копать.

Эта статья — про неожиданные находки. Не про «как я выбрал стек» (скучно) и не про «туториал по WebRTC» (и без меня есть). Это пять ситуаций, в которых я споткнулся, обнаружил что-то интересное, и подумал «об этом стоит рассказать — другим пригодится».

Поехали.


1. Одна строчка в robots.txt, которая съедает crawl-бюджет Google

Свежий кейс. Смотрю логи сервера — Googlebot тысячу раз за сутки дёргает внутренний XHR-эндпоинт, который фронтенд вызывает при рендеринге. Думаю: «Стоп. Как он вообще туда попал? У меня же в robots.txt запрещено.»

Открываю robots.txt. Читаю. Вроде всё правильно:

User-agent: *Disallow: /api/Disallow: /admin/Disallow: /profile/# ... ещё 15 запретовUser-agent: YandexBotCrawl-delay: 1Allow: /User-agent: GooglebotAllow: /

Disallow: /api/

Disallow: /admin/

Disallow: /profile/

# ... ещё 15 запретов

User-agent: YandexBot

Crawl-delay: 1

Allow: /

User-agent: Googlebot

Allow: /

Логика в голове: «для всех ботов запрещаю то-то, а для именных — разрешаю всё».

А вот нихрена. По RFC 9309 и всему здравому смыслу robots-стандарта бот использует ровно один блок — самый специфичный к его User-Agent. Если есть блок Googlebot — общий * полностью игнорируется.

То есть Googlebot видел одно:

User-agent: GooglebotAllow: /

Allow: /

И радостно шёл индексировать весь /api/*. Включая служебные эндпоинты. Включая трекинг показов рекламы — в статистике показов рекламы у меня стали появляться впечатления от бота, с IP из диапазона Google Crawler.

Фикс простой и неприятный

Нужно продублировать Disallow в каждую именованную секцию:

User-agent: *Allow: /Disallow: /api/# ...User-agent: YandexBotCrawl-delay: 1Allow: /Disallow: /api/   ← это было пропущено# ...User-agent: GooglebotAllow: /Disallow: /api/   ← и это# ...

Allow: /

Disallow: /api/

# ...

User-agent: YandexBot

Crawl-delay: 1

Allow: /

Disallow: /api/ ← это было пропущено

# ...

User-agent: Googlebot

Allow: /

Disallow: /api/ ← и это

# ...

Мораль: если у вас есть именованные секции, каждая должна быть самодостаточной. Не наследует от *. Многие сайты на это попадаются — я проверил robots.txt у десятка русскоязычных SaaS-проектов, у трёх нашёл ту же самую ошибку.

Проверьте свой. Серьёзно.


2. Математика вкуса: почему 24 оси, а не 5 «жанров», и при чём тут косинус

В обычной рекомендательной системе вкус ужимают до жанров: «любит боевики, не любит мелодрамы». Летает на нас с анонсами Netflix «because you watched Fast & Furious» — и промахивается.

Промахи не потому, что ML плохой. Потому что жанр — это на уровне каталога, а не вкуса. Два фильма одного жанра могут быть полярно разными для конкретного человека. «Бегущий по лезвию 2049» и «Трансформеры» — оба «фантастика+боевик». Зайти к любителю «Бегущего» с «Трансформерами» = обидеть.

Что я сделал в Cinema DNA

Разложил вкус на 24 оси — не жанровых, а структурных. Примеры по категориям:

Темп и композиция

  • Темп повествования

  • Терпимость к длинным сценам

  • Сложность нарратива (линейность ↔ фрагментированность)

Эмоциональная природа

  • Эмоциональная плотность

  • Нужен ли катарсис

  • Насколько тяжёлые темы готов смотреть

Форма против содержания

  • Важность визуального стиля

  • Толерантность к авторскому почерку

  • Играет ли фильм с формой

Контекст просмотра

  • Оптимальная длина

  • В компании / одному

  • Настроение

Каждая ось — ползунок. На выходе теста — точка в 24-мерном пространстве.

Почему комбинация метрик, а не только косинус

Первое, что приходит в голову для поиска «похожих» пользователей — косинусная близость: углы между векторами. Работает, но даёт контринтуитивные результаты в граничных случаях.

Простой пример: человек А ответил на все вопросы «50/100» (нейтрально, не задумываясь), а человек Б ответил честно, с разбросом. Косинус между их векторами может выйти 0.95+ (по направлению близки), но на практике это два совершенно разных человека — один не определился, другой имеет чёткий профиль.

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

  • Два случайных юзера — 35-55%

  • Юзеры с пересечением любимых фильмов — 80-95%

  • Полярные профили — ниже 20%

Конкретные веса и нормализацию тюнил эмпирически. Это не открытие, просто напоминание: когда кажется, что одной метрики достаточно — скорее всего, её недостаточно.

Масштабирование

Пока юзеров немного, поиск похожих работает полным перебором O(N). На сотнях тысяч переведу на pgvector с ANN-индексом (ivfflat для старта, hnsw если понадобится качество). Это известная задача, решение найдено, просто до этой точки ещё ехать.


3. Синхронный просмотр: пять вещей, на которых я споткнулся с WebRTC

Фича: «смотрим фильм вместе, когда в разных городах». Два человека заходят в комнату, загружают свою копию фильма, смотрят синхронно, болтают голосом. Звучит как «ну WebRTC + play/pause-события, 2 дня работы». В реальности — три недели.

Пять неожиданных проблем, с которыми столкнулся:

3.1. NAT traversal не решается «из коробки»

Голый peer-to-peer без TURN-сервера не пробивает NAT у 40% домашних роутеров. Я наивно начинал с STUN-only и нескольких итераций «работает у меня, у брата не работает». Причина — симметричный NAT, классика мобильных операторов и некоторых провайдеров.

Решение — свой TURN-сервер. Opensource реализации есть, разворачиваются за вечер. Без TURN — не берите за фичу P2P-видео «для всех пользователей». Только для своих, у кого IPv6 или нормальный NAT.

3.2. captureStream() на  — не волшебная палочка

Да, из DOM-элемента <video> можно получить MediaStream и отправить по WebRTC. Работает. Но:

  • DRM-контент не отдаётся. Вы не сможете стримить Netflix-видео из тега <video> — там поток через EME, и captureStream вернёт либо пустоту, либо чёрный кадр.

  • Браузеры отдают разные частоты кадров. Chrome честно стримит 30-60fps, Firefox иногда ограничивает. Safari до недавнего времени вообще не поддерживал captureStream для HLS-видео.

  • Аудиодорожки в некоторых форматах (многоканальные Dolby) захватываются криво или не целиком.

Урок: всегда тестируйте на 3-4 браузерах с реальными форматами контента, не только с .mp4 + h264 + aac.

3.3. Autoplay policies на гостевой стороне

Когда хост играет видео, гостю приходит поток через WebRTC. Вы делаете guestVideo.srcObject = stream и думаете «всё, готово». Видео не стартует.

Почему: современные браузеры блокируют автоплей видео со звуком без пользовательского действия. Даже если поток пришёл вживую. Решение — гостю нужно самому нажать Play (или muted=true на старте, потом unmute по клику).

Это звучит банально, но я потратил полдня отладки, пока не вспомнил.

3.4. Голос и видео — лучше два отдельных peer connection

Соблазн: один peer connection, в нём и видео фильма, и голоса. Меньше overhead, проще код.

Реальность: юзеры постоянно хотят мьютить звук фильма, чтобы слышать собеседника. С одним peer это превращается в manage отдельных audio track enable/disable, состояния синхронизировать между участниками, и т.д.

Два независимых peer (один — видео фильма с видео+аудио, второй — только голос-peer) — кардинально упрощают UX-логику. Да, немного больше нагрузки на сеть. Но вы получаете чистое разделение «это фильм — могу заглушить» и «это собеседник — не могу потерять».

3.5. Синхронизация play/pause — это не просто «посылай событие»

Хост жмёт паузу в 15:42:18.123. Событие летит через WebSocket на сервер. Сервер рассылает гостям. Гость получает его в 15:42:18.456.

Если у гостя в момент прихода события currentTime = 15:42:18.410 (потому что у него видео шло своим ходом с минимальным дрейфом), а вы просто делаете guestVideo.pause() — они будут стоять на разных кадрах.

Нужно не просто паузить, а выравнивать currentTime по хосту с компенсацией latency. Плюс периодический re-sync — каждые 10 секунд хост шлёт свой currentTime, гости, если ушли на >300ms, подтягиваются.

Мелочь, но именно эти мелочи отделяют «работает прикольно» от «работает и ты забываешь, что фильм показывается через сеть».


4. SEO-грабли на Next.js 14: ISR и 11-тысячный sitemap

Если у вас на сайте больше 1000 страниц контента, которые попадают в sitemap — внимание.

Next.js App Router позволяет сделать sitemap.xml как route в app/sitemap.ts. Вы возвращаете массив URL, Next строит XML. Красиво.

Подводный камень: этот route работает через ISR (incremental static regeneration). Если не поставить export const revalidate = <big number>, то каждый запрос sitemap заставляет пересобирать весь список URL. А чтобы собрать URL, нужно сходить в БД за всеми 11 000 фильмами, 2000 персонами, 300 коллекциями. Каждый запрос.

Я сначала поставил revalidate = 1800 (30 минут), думая «sitemap же не критично свежий, 30 мин хватит». Получил ситуацию: каждые 30 минут мой API генерит 11k-URL ответ на запрос sitemap от GoogleBot, YandexBot, каждого curl от мониторинга и т.д.

Поднял до 86400 (24 часа). Стало нормально. Крутить выше — не надо, Google может решить что ваш sitemap stale.

Ещё подробность: структурированные данные

JSON-LD с @type: Movie и @type: Person на страницах фильмов/людей — сильно улучшает показы в rich results Google. Не поленитесь. Правильные schema.org-поля (actor, director, contentRating, ratingValue) Google использует в карточках поиска. Это +15-30% CTR на страницу фильма по моим наблюдениям.


5. IndexNow — API, который реально работает (и его почему-то мало используют)

IndexNow — протокол, который поддерживают Bing, Yandex, Seznam, Naver и через который вы можете пинговать поисковики при публикации новой страницы. Индексация — за минуты вместо недель.

Протокол простой до идиотизма:

  1. Генерируете случайный ключ

  2. Кладёте файл {ключ}.txt в корень сайта

  3. Шлёте POST на https://api.indexnow.org/indexnow с JSON {host, key, urlList}

Всё. На это у меня ушёл час, включая тесты.

Google IndexNow не поддерживает (у них свой API через Search Console, ручной). Но Bing + Yandex — это уже ~15% российского поискового трафика + международный. И через Bing по цепочке идёт индексация в ChatGPT, DuckDuckGo, Copilot — для LLM-видимости это значимо.

Странно, что об IndexNow так мало пишут. Возможно потому, что Google не в клубе, и русские технические блоги этот протокол часто игнорируют. Зря.


Что в итоге

Пять историй, пять граней одной и той же мысли: проект, который снаружи выглядит «стандартный CRUD + REST», внутри содержит десятки нетривиальных технических решений, про которые в учебниках не написано.

Если интересно посмотреть результат — VibeMuvik открыт, пользование бесплатное, регистрация — минута. Буду рад багрепортам, идеям и критике архитектурных решений.

Хочу написать ещё — есть темы:

  • Как устроены батлы и дебаты (голосование с аргументами, почему это не просто «опрос»)

  • Производительность PostgreSQL: партициирование по времени, материализованные представления для городской статистики, когда CREATE INDEX CONCURRENTLY спасает, а когда — нет

  • Анализ ошибок авторизации и 2FA: что я узнал, читая свой audit log

Если хоть одна из этих тем интересна — пишите в комментариях, какую развернуть следующей.

Спасибо за чтение.

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