
Летом я сделал сайт, который собирает публичные Telegram‑каналы российских стримеров в одну ленту. Идея была простая: я подписан на десяток таких каналов и устал листать их по очереди, чтобы узнать, будет ли сегодня стрим.
Через три месяца у проекта 260 коммитов, 450 каналов, больше 60 тысяч постов в базе, около 4 тысяч страниц в поиске Яндекса и доход от рекламы — 1–2 рубля в день. Это честная ретроспектива: что сработало, на какие грабли я наступил и что показали данные про то, зачем люди на самом деле приходят на такой сайт. Спойлер: не за тем, для чего я его делал.
Стек: максимально скучно
PHP 8.2 без единой зависимости — ни Composer, ни фреймворка;
SQLite: одна база на всё, полнотекстовый поиск через FTS5;
обычный shared‑хостинг за пару сотен рублей в месяц;
деплой —
git pullпо крону раз в час, следом тем же кроном запускается парсер.
Звучит архаично, но для проекта, который может не взлететь, это правильный выбор: никакой инфраструктуры, которую надо обслуживать. Почти все проблемы, о которых пойдёт речь ниже, при этом выросли именно из дешёвого хостинга.
Парсинг Telegram без API
У каждого публичного канала есть веб‑версия: t.me/s/<канал>. Это обычная HTML‑страница с последними постами, просмотрами и реакциями. Ни бот, ни API‑ключ, ни аккаунт не нужны. Парсер — DOMDocument и DOMXPath:
$dom = new DOMDocument(); @$dom->loadHTML('<?xml encoding="utf-8"?>' . $html); $xp = new DOMXPath($dom); foreach ($xp->query('//div[contains(@class,"js-widget_message") and @data-post]') as $msg) { // id поста, дата, текст, фото, превью видео, просмотры, реакции }
Пара неочевидных моментов:
Несуществующий канал не отдаёт 404. Telegram молча редиректит на свою промо‑страницу, поэтому «канал жив» определяется по наличию заголовка канала в разметке, а не по HTTP‑коду.
Ссылки на картинки протухают. Фото лежат на CDN Telegram по временным ссылкам. Если не скачать картинку в момент парсинга, через какое‑то время её уже не достать. Поэтому парсер качает медиа сразу.
Просмотры растут в основном первые сутки. Поэтому ежечасно парсится только первая страница каждого канала, а раз в сутки ночью идёт глубокий проход, который освежает цифры у старых постов.
Грабли
Самое полезное в этом проекте — список того, что сломалось. Почти всё ломалось молча.
t.me пропал из DNS
13–14 июля домен t.me на время исчез из DNS‑зоны .me — на уровне реестра, не у Telegram. Парсер встал. Спасло то, что домен для парсинга был вынесен в конфиг: переключил на telegram.me и вернул обратно, когда t.me восстановили. Вывод банальный, но я его выучил на практике: любой внешний хост — в конфиг.
CDN Telegram заблокирован у части провайдеров
У части российских провайдеров CDN Telegram недоступен. Картинки на сайте и og‑превью в шарингах оказывались битыми — и только у части посетителей, что особенно весело отлаживать.
Решение в два слоя. Парсер качает картинки через свой прокси в Нидерландах и кладёт их на хостинг. А живые страницы сайта не ходят ни на прокси, ни на CDN Telegram никогда: нет локальной копии — картинка просто не показывается. Посетитель всегда получает всё с одного домена.
“database is locked”
Парсер по крону пишет в SQLite, веб в это время читает — и время от времени пользователи ловили 500-ки. Лечится стандартно:
$pdo->exec('PRAGMA journal_mode=WAL'); // чтение параллельно с записью $pdo->exec('PRAGMA busy_timeout=5000'); // подождать вместо ошибки $pdo->exec('PRAGMA synchronous=NORMAL'); // обычная пара к WAL
Побочный эффект WAL: бэкап простым копированием файла становится неполным, часть транзакций живёт в -wal‑файле. Бэкап — только через VACUUM INTO (а на старых версиях SQLite — через Online Backup API).
Шесть дней без новых постов, и никто не узнал
С 11 по 16 августа на проде не работал git pull: я переписал ~/.ssh/config под другой проект и потерял секцию с ключом этого. Парсер запускался через && следом за git pull и встал вместе с ним.
У меня был healthcheck, который каждый день проверял, идут ли посты, и слал письмо. Он честно отправлял его шесть дней подряд. Не дошло ни одно: почта с shared‑хостинга без SPF и DKIM молча режется на стороне получателя. Теперь алерты уходят в Telegram‑бота, а у healthcheck есть ключ --test-notify, чтобы проверить сам канал оповещений.
Мораль: мониторинг, который ни разу не проверяли на настоящей поломке, — это не мониторинг.
У сайта не было og‑картинок, а я не знал
Картинки для шаринга рисуются через GD, и для текста нужен TTF‑шрифт. На макбуке всё работало: функция поиска шрифта находила системный Arial. На проде шрифта не было, рендер возвращал null, и до 17 августа все ссылки на сайт расшаривались без картинки. Шрифт теперь лежит в репозитории (Roboto, Apache 2.0).
Роут, которого нет на проде
На локальной машине роутингом занимается router.php для встроенного сервера PHP, на проде — .htaccess с mod_rewrite. Новую страницу событий я добавил в первое и забыл во второе. 34 страницы из sitemap отдавали 404, тесты были зелёными: смоук‑тесты гоняются на dev‑сервере, то есть через router.php.
Теперь отдельный тест разбирает .htaccess регулярками и проверяет, что у каждого красивого URL есть своё правило, а не общий фолбэк на 404.
130 тысяч файлов из 150 тысяч
На хостинге есть лимит не только на место, но и на число файлов — 150 тысяч на аккаунт. Однажды выяснилось, что проект занял 130 тысяч. Виноваты две вещи:
каждая картинка поста лежала отдельным файлом: около 70 тысяч, плюс 15–20 тысяч каждый месяц;
og‑картинка для каждого поста, которую хоть раз запросил робот, кешировалась на диск отдельным PNG.
Чистка помогла бы на пару месяцев. Поэтому картинки переехали в отдельную SQLite‑базу — строками с BLOB. Для хостинга 70 тысяч файлов превратились в один. Адреса картинок не изменились: если файла на диске нет, .htaccess отправляет запрос в маленький PHP‑скрипт, а тот отдаёт картинку из базы с вечным кешем.
RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^assets/media/([A-Za-z0-9_]+/\d+-(?:p\d+|thumb))\.(webp|jpg)$ media.php?k=$1&ext=$2 [L]
header('Cache-Control: public, max-age=31536000, immutable'); header('ETag: ' . $etag); if (($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') === $etag) { http_response_code(304); exit; }
Картинка под ключом не меняется никогда, поэтому браузер запрашивает её один раз. Ещё не перенесённые файлы всё это время отдаются статикой как раньше, так что миграцию можно запускать когда угодно после деплоя. А og‑картинки постов теперь рисуются на лету и вообще не сохраняются: их запрашивают почти одни роботы, по разу на пост.
«Что обсуждают»: медиана вместо среднего
Одна из страниц сайта ищет посты, которые обошли обычный уровень своего канала — без редактора, по цифрам. Сначала нормой было среднее число просмотров канала за полгода. Порог к нему пропускал 13 постов в месяц: один вирусный пост задирал среднее, и после этого канал полгода не мог «выбиться из нормы».
Перешёл на медиану со сглаживанием (константа в знаменателе гасит мелкие каналы, где 300 просмотров против медианы 100 — ещё не событие):
"p.views * 1.0 / (COALESCE(c.median_views, 0) + " . VIEWS_RATIO_SMOOTHING . ")"
Материал на странице стал появляться в 46% дней вместо 31%. Порог ниже 2,0 опускать нельзя: в полосе 1,5–2,0 живут анонсы видео и реклама, то есть обычный разброс.
Что показал Яндекс
Теперь самое интересное. Цифры из Вебмастера и Метрики на начало октября:
в поиске около 4 300 страниц;
около 7 500 показов в выдаче в день, около 105 кликов;
средняя позиция — около 9;
внешних ссылок на сайт — ноль, ИКС — 10.
И главное — с какими запросами приходят люди. Я делал ленту постов. Люди ищут вот это:
Запрос |
Показы за 2 недели |
Клики |
|---|---|---|
сколько лет гленту |
348 |
0 |
сколько лет булкину |
298 |
0 |
сколько лет даниле горилле |
291 |
1 |
как зовут глента |
254 |
17 |
где живет эвелон |
93 |
0 |
Три четверти показов и 84% кликов приходятся на страницы стримеров, и почти все запросы — вопросы о человеке: возраст, настоящее имя, откуда он. Главную с лентой из поиска не находит практически никто, возвращается на сайт 6% посетителей.
То есть продукт, который нужен людям, — не агрегатор постов, а справочник «кто это и что с ним сейчас». Посты из Telegram при этом не бесполезны: это единственный источник, который отвечает на вторую половину вопроса, и этого нет у вики‑агрегаторов.
Заголовок, который отвечает слишком хорошо
Заметил странное: у запросов про возраст CTR около 1,4%, хотя ответ на странице есть. Причина — ответ был и в title: «Имя — настоящее имя такое‑то, 21 год». Человек читал ответ прямо в выдаче и не кликал. Для сравнения, у трёх лучших страниц сайта в сниппете обрывок текста с многоточием, то есть обещание, которое можно получить только внутри, — и CTR у них на порядок выше.
Сейчас идёт эксперимент: на 13 профилях с худшим CTR title не отвечает, а обещает то, чего нет в фактовом блоке Яндекса, — что про человека пишут другие каналы. Четыре из тринадцати не подошли по условию и стали контрольной группой сами собой. Итоги — в конце октября.
Как я неправильно прочитал собственный отчёт
У меня есть скрипт, который считает, какие страницы и почему закрыты от индекса. В сентябре он показал ноль по двум правилам, и я решил, что их пороги мёртвые и правила можно убрать. Оказалось, отчёт работал на локальной копии базы, которой было 47 дней, а оба правила требуют свежести 30 дней. Кандидатов не могло быть в принципе. На живой базе эти правила держат около 75 страниц.
Теперь отчёт первой строкой печатает дату самого свежего поста и прямо предупреждает, если снимок старше окна свежести. Нули на устаревших данных — самый коварный вид неправды: выглядят как вывод.
Деньги
РСЯ приносит 1–2 рубля в день. Это не опечатка. При ~260 просмотрах в день и молодой игровой аудитории монетизации пока нет и не может быть. Ориентир для себя я поставил такой: сначала дойти до 500–1 000 кликов из поиска в день, и только потом думать о деньгах.
Самый большой тормоз сейчас — ноль внешних ссылок. Сайту три месяца, на него никто не ссылается, и позиции застряли около девятого места. Это лечится не кодом.
Как писал
Большую часть кода я писал в паре с ИИ‑ассистентом (Claude Code), и это хорошо видно по проекту: 260 коммитов за три месяца в свободное время. Самое полезное оказалось не в скорости написания кода, а в дисциплине вокруг него: каждый коммит объясняет, какую проблему решает и какими цифрами это подтверждено, у гипотез есть дата замера, а у каждого инцидента — тест, который не даст ему повториться.
Выводы
Скучный стек на shared‑хостинге — нормальный выбор для проекта, который может не взлететь. Но лимиты хостинга (число файлов, почта без SPF) надо знать заранее.
Всё, что может сломаться молча, сломается молча. Проверяйте мониторинг настоящей поломкой.
Окружение разработки и прода должны совпадать хотя бы в шрифтах и роутинге — или тесты должны проверять разницу.
Смотрите, с какими запросами к вам приходят, раньше, чем решите, что делаете. Я три месяца строил ленту, а пользователям нужен справочник.
Сайт — off‑stream.ru. Буду рад вопросам в комментариях, особенно про парсинг и SQLite на shared‑хостинге.
Комментарии (4)

andy2000
15.10.2026 09:18Когда долистал до .htaccess, как-то больно стало. Неужели апачем кто-то ещё пользуется.... Корпоративные среды мимо
Marfapr
я подписан на десяток таких каналов и устал листать их по очереди, чтобы узнать, будет ли сегодня стримА я для этого сделал просто бота-уведомлятора с гибкой настройкой.