Как мы починили уведомления о новых постах
Разбираем, как перевели подписку на блог из фальшивой формы в реальные email и Telegram-уведомления.
Когда мы заметили проблему с формой «Подписаться на блог», оказалось, что там вообще не было рабочей подписки. Для залогиненного пользователя интерфейс просто проверял сессионную куку и показывал сообщение вроде «Вы уже подписаны». Но за этим не стояло ни одной записи в базе и ни одного механизма отправки уведомлений при публикации нового поста.
Для блога это важная часть цепочки. Мы не хотели оставлять подписку как декоративную кнопку. Если человек явно выбрал получать обновления, система должна это помнить, показывать состояние и использовать его в момент публикации. Поэтому мы вынесли это в отдельный проект «Уведомления» и сделали его как нормальную часть продукта, а не как временный костыль.
Что именно было сломано
Проблема была не только в форме. Она тянулась сразу в несколько мест.
Во-первых, у нас не было явной модели подписки на блог. Были пользователи, были сессии, но не было настоящего состояния opt-in по каналам. Во-вторых, при публикации поста не существовало шага, который бы собирал список подписчиков и отправлял им письмо или сообщение в Telegram. В-третьих, кабинет и админка тоже не знали, как отображать это состояние, потому что смотреть было не на что.
То есть мы жили в ситуации, где фронт показывал «всё нормально», а бэкенд ничего не делал. Такие вещи опасны именно тем, что долго выглядят рабочими. Ошибка не падает, пользователи не жалуются сразу, а потом выясняется, что канал коммуникации был пустым с самого начала.
Как мы разложили уведомления по каналам
Мы решили не делать одну общую галочку «получать письма», а разделить каналы отдельно: email и Telegram. Это важный момент для продукта, потому что поведение у этих каналов разное, и пользователь должен управлять ими независимо.
На уровне базы мы добавили в таблицу users два поля: blog_notify_email и blog_notify_telegram. Оба поля работают как opt-in флаги. Это простой вариант хранения, и для нашей задачи он оказался самым прямым: нам не нужно было придумывать отдельную сложную сущность, если главное — понять, включён ли канал у конкретного пользователя.
При этом мы добавили серверную проверку. Пользователь не может включить email-уведомления, если у него нет email, и не может включить Telegram, если у аккаунта нет telegram_id. Это важно, потому что интерфейс сам по себе не должен обещать то, чего система не сможет выполнить.
Мы отдельно продумали и дефолтное поведение для новых регистраций, которые приходят именно из блога. Если человек регистрируется через /register?source=blog, ему включается email-путь. Если он проходит Telegram-вход через /auth/telegram/callback?source=blog, ему включается Telegram-путь. Это не означает, что мы что-то рассылаем без согласия — это значит, что регистрация из блога сразу связывается с каналом, через который человек пришёл.
Как мы починили саму форму подписки
Корень бага был на фронте: компонент BlogSubscribeForm.astro для уже залогиненных пользователей вообще не ходил в API. Он просто рисовал статичное сообщение и на этом заканчивал работу.
Мы заменили это поведение на нормальный запрос к /api/blog-subscription. Теперь форма читает реальное состояние пользователя и показывает два чекбокса — Email и Telegram — с текущими значениями. Если канал доступен, его можно включить или выключить. Если канала нет, чекбокс не даёт включить то, что сервер всё равно отверг бы.
Это кажется мелкой правкой, но на самом деле она меняет логику интерфейса: форма перестаёт быть заглушкой и становится точкой управления подпиской. Мы не просто говорим человеку «вы уже подписаны», а показываем, на что именно он подписан.
Где ещё мы дали управление пользователю
Одной только страницы блога мало. Подписка — это не одноразовое действие, и человек должен уметь управлять ей в кабинете.
Поэтому мы добавили в /cabinet отдельную карточку «Рассылка блога» с теми же двумя чекбоксами. Там используется тот же API, что и в форме блога. Это полезно сразу по двум причинам: логика не дублируется, а пользователь получает ещё одно место, где может проверить свои настройки.
Параллельно мы расширили админку /admin/subscribers. Там появились счётчики по email и Telegram opt-in уже по всей базе, а не только по тем, кто когда-то пришёл с blog acquisition source. Это важный нюанс: подписка на канал может включаться и позже, уже после регистрации, поэтому смотреть только на источник привлечения было бы неверно.
В таблице подписавшихся через форму блога мы добавили отдельные колонки, чтобы видеть состояние email и Telegram по каждому пользователю. Для нас это не просто отчётность, а способ быстро понять, где подписка включена, а где нет.
Как мы связали публикацию поста с отправкой уведомлений
Следующий шаг — научить систему реагировать на публикацию нового поста.
Для этого мы добавили внутренний endpoint /internal/blog/notify-published. Он закрыт внутренним токеном BLOG_NOTIFY_INTERNAL_TOKEN в заголовке X-Internal-Token. Это не публичный маршрут по смыслу: его не должен дергать браузер, только наша публикационная цепочка.
Дальше мы дописали scripts/publish_next.py. После успешного build, deploy и git commit скрипт парсит title и description из frontmatter и отправляет POST на локальный сервис. Важно, что это сделано как best-effort: если уведомление по какой-то причине не ушло, сам успешный релиз поста не откатывается. Мы не хотели смешивать публикацию контента и доставку уведомлений в один жёсткий блокирующий шаг.
Чтобы это работало стабильно, мы положили токен в окружение и подключили EnvironmentFile к systemd-сервису публикации. Так публикационный процесс получил доступ к тому же секрету, что и бэкенд, без ручных вставок в код.
Что мы проверили и что оставили на следующий шаг
Email-канал мы протестировали на живом аккаунте. Включили blog_notify_email=1, вручную вызвали внутренний endpoint с тестовым постом и получили реальный ответ от системы отправки писем. Это важная точка: мы проверили не только JSON-ответ, но и сам путь через SMTP, который у нас уже был настроен раньше для сброса пароля.
Telegram мы сознательно не смогли довести до такой же проверки в этой сессии. На тестовом аккаунте не было привязанного telegram_id, а связка Telegram должна появляться через реальный вход пользователя. Мы не стали подменять это фиктивными данными, потому что тогда проверка перестала бы что-либо значить.
Отдельно мы не развернули изменения в Astro-репозитории на прод сразу. Там в рабочем дереве уже были чужие незакоммиченные правки от параллельной сессии, и мы не хотели перетирать их механически. Поэтому фронтовые изменения для продакшена оставили на следующий шаг, когда можно будет спокойно разрулить состояние репозитория.
Что это меняет для нас дальше
Теперь у блога есть нормальная подписка с реальными каналами, а не видимость подписки. У нас есть хранение состояния, управление из формы блога и кабинета, контроль в админке и связка с публикацией поста. Это уже похоже на рабочий продуктовый контур, а не на набор разрозненных правок.
Следующий логичный шаг — довести Telegram-проверку на живом аккаунте и аккуратно выкатить фронтовые изменения на прод, когда рабочее дерево будет чистым. После этого уведомления станут частью стандартного процесса публикации, а не отдельной ручной операцией.
Если вам интересно, как мы строим такие связки между продуктом, кабинетом и автоматизацией вокруг блога, смотрите также страницу услуг.
Рассылка новых статей
Выберите, куда присылать новые статьи и разборы — без спама, только новые материалы.
Чтобы получать статьи на email, сначала привяжите email в личном кабинете.
Чтобы получать статьи в Telegram, войдите через Telegram-кнопку на странице входа.
Получайте новые статьи и разборы
Подпишитесь через email — без спама, только новые материалы.