Оглавление

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

В качестве второй площадки я выбрал MAX. Дальше была цель простая: пост вышел на сайте, пост улетел в канал. Без копипаста, без “сейчас, минутку, я руками перекину”.

В итоге я собрал рабочую схему и дописал свой плагин под WordPress. Ниже показываю путь от создания канала и получения токена до очереди отправки и загрузки картинок через API.

Что я хотел получить по факту

Мне нужна была не “интеграция ради интеграции”, а практичный автопостинг.

1) Публикация новых записей из WordPress в канал MAX.

2) Нормальный внешний вид: заголовок, короткий текст, ссылка, кнопка “Читать”.

3) Картинка к посту, чтобы лента выглядела как медиа, а не как лог с сервера.

4) Очередь, повторы и логи. Чтобы редактор нажал “Опубликовать”, а дальше уже работала автоматика, даже если сеть моргнула.

Этап 1. Канал и доступ к партнёрской панели

С MAX всё начинается с партнёрской панели. Там создаётся канал и там же выдаётся токен для интеграции. Без токена дальше делать нечего.

Как я настроил автопостинг из WordPress в MAX и почему это оказалось проще, чем кажется
Стартовая страница партнёрской панели.

Важно: я сразу завёл отдельного бота именно под автопостинг. Не смешиваю в одну кучу “бот отвечает людям” и “бот постит новости”. Так проще поддерживать и безопаснее.

Этап 2. Токен, права и главный нюанс с админством

После создания бота мне понадобились две вещи: токен и идентификатор канала (Chat ID). Токен это ключ от двери. Chat ID это адрес, куда стучаться.

Самый частый затык на старте выглядит так: токен есть, запросы уходят, а посты не публикуются. Причина банальная. Бот должен быть администратором канала. Если он просто добавлен “как участник”, права на публикацию могут не сработать.

Как я настроил автопостинг из WordPress в MAX и почему это оказалось проще, чем кажется
Окно, где выдаётся токен интеграции.

Я это проверял просто: отправлял тестовое сообщение из админки плагина. Если тест уходит, значит токен рабочий, Chat ID правильный, права выставлены.

Этап 3. Админка WordPress и настройки плагина

Я сделал отдельную страницу настроек в админке WordPress. Минимум полей, максимум пользы: Token, Chat ID, галка “картинка”, галка “кнопка”, плюс кнопка “Отправить тест”.

Как я настроил автопостинг из WordPress в MAX и почему это оказалось проще, чем кажется
Настройки плагина в WordPress.

Токен я храню в настройках сайта и не свечу нигде на фронте. Доступ к странице настроек только у админов. На сохранение настроек повесил nonce, потому что я люблю, когда чужие руки не трогают мои ключи.

Как устроена отправка постов и почему я сделал очередь

Сначала я пробовал отправлять пост “в момент публикации”. Работает, пока всё идеально. Потом ловишь ситуацию: сеть зависла, API ответило медленно, редактор висит, WordPress думает, что жизнь боль, и ты начинаешь ненавидеть всё живое.

Поэтому я сделал очередь.

При публикации поста я не отправляю его сразу. Я ставлю метку “в очереди” (post meta) и складываю ID в список. Дальше по расписанию это разгребает фоновая задача (WP Cron или системный cron, как настроишь).

Плюсы простые: админка не тормозит, посты уходят стабильно, при ошибке можно повторить попытку, логи сохраняются.

Как я настроил автопостинг из WordPress в MAX и почему это оказалось проще, чем кажется
Момент создания канала, чтобы было видно живьём.

Картинка к посту: почему просто URL это не всегда надёжно

Самая “весёлая” часть была с картинками. Хотелось сделать по взрослому: брать featured image из WordPress и прикреплять к сообщению так, чтобы в MAX она отображалась нормально.

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

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

Как я настроил автопостинг из WordPress в MAX и почему это оказалось проще, чем кажется

Форматирование: один пост или два сообщения

Я быстро понял, что у формата “картинка плюс длинная простыня” есть побочные эффекты. Где то подпись обрезалась, где то выглядело не так, как я ожидал на телефоне.

Поэтому я сделал два режима.

Режим 1: одно сообщение, где картинка и подпись вместе.

Режим 2: сначала улетает картинка, следом улетает текст с заголовком, анонсом и кнопкой “Читать”. На практике второй вариант выглядит аккуратнее и стабильнее.

Пример логики отправки (упрощённо)

Ниже упрощённый пример того, как выглядит “ядро” отправки: собрал текст, загрузил картинку, сформировал payload, отправил запрос. В реальном плагине вокруг этого ещё очередь, повторы и логирование.

public static function send_post_to_max(int $post_id): bool {
    $s = self::get_settings();

    $title = get_the_title($post_id);
    $url   = get_permalink($post_id);

    $raw = get_post_field('post_content', $post_id);
    $text = wp_trim_words(wp_strip_all_tags($raw), 45);

    // 1) Загружаю featured image и получаю токен (если есть картинка)
    $img_token = null;
    if (!empty($s['with_image'])) {
        $img_token = self::upload_featured_image_and_get_token($post_id);
    }

    // 2) Формирую сообщение
    $msg = "<b>" . esc_html($title) . "</b>\n\n"
         . esc_html($text) . "\n\n"
         . esc_html($url);

    $payload = [
        'chat_id' => $s['chat_id'],
        'text'    => $msg,
        'attachments' => []
    ];

    // 3) Прикрепляю картинку по токену
    if ($img_token) {
        $payload['attachments'][] = [
            'type' => 'image',
            'payload' => ['token' => $img_token]
        ];
    }

    // 4) Кнопка Читать
    if (!empty($s['with_button'])) {
        $payload['attachments'][] = [
            'type' => 'inline_keyboard',
            'payload' => [
                'buttons' => [
                    [
                        [
                            'type' => 'link',
                            'text' => 'Читать',
                            'url'  => $url
                        ]
                    ]
                ]
            ]
        ];
    }

    // 5) Отправляю
    $resp = wp_remote_post(self::api_url('/messages.send'), [
        'timeout' => 15,
        'headers' => [
            'Authorization' => 'Bearer ' . $s['token'],
            'Content-Type'  => 'application/json; charset=utf-8',
        ],
        'body' => wp_json_encode($payload, JSON_UNESCAPED_UNICODE),
    ]);

    return !is_wp_error($resp) && wp_remote_retrieve_response_code($resp) >= 200
           && wp_remote_retrieve_response_code($resp) < 300;
}

Да, API endpoints и названия методов могут отличаться в зависимости от текущей версии. Суть тут в архитектуре: не блокировать публикацию, грузить картинку файлом, логировать ответ, уметь повторить.

Итог

В итоге я получил нормальную связку: WordPress публикует, плагин ставит задачу в очередь, фоновая отправка пушит пост в MAX, а подписчики видят красивый пост с кнопкой. Это ровно тот случай, когда один вечер разработки экономит кучу времени дальше.

Дальше я планирую допилить массовую публикацию старых записей с выбором “с новых к старым” и сделать более удобный экран очереди. Чтобы канал можно было наполнить за час, а не за неделю.