Проблема: ручной постинг — прошлый век

До автопостинга мой процесс выглядел так:

  1. Написал статью на сайте
  2. Открыл VK → скопировал текст → вставил → прикрепил картинку → опубликовал
  3. Открыл Telegram → скопировал текст → переформатировал (другая разметка) → опубликовал
  4. Повторил для каждого нового поста

Это отнимало 15-20 минут на каждый пост. При 3-4 постах в неделю — час тупой рутины. Умножь на год — 50 часов жизни на копипасту.

Альтернативы, которые я рассматривал и отбросил:

ВариантЦенаПочему нет
SMM-менеджер на аутсорсе15 000-30 000 ₽/месДорого, медленно, зависимость от человека
SMMplanner / Amplifr900-4 000 ₽/месОграничения по аккаунтам, чужая инфраструктура
Готовые WordPress-плагины0-2 000 ₽/месЗавязка на WP, не кастомизируется

Архитектура: 3 компонента

┌──────────────────────┐
│   Админка (PHP)      │  ← Создаю пост: заголовок, текст, картинка
│   admin/social.php   │    Выбираю: VK / Telegram / оба
└──────────┬───────────┘
           │ INSERT в tg_queue
           ▼
┌──────────────────────┐
│   VK Poster (PHP)    │  ← Публикует сразу через VK API
│   SocialPoster.php   │    wall.post + photos.getWallUploadServer
└──────────────────────┘
           │
           ▼
┌──────────────────────┐
│ TG Queue Worker      │  ← Node.js, cron каждые 2 минуты
│ tg_queue_worker.js   │    Забирает посты из очереди → публикует в Telegram
└──────────────────────┘

Компонент 1: Админка (PHP)

Страница в админке, где я создаю пост. Минимальная форма:

<?php
// admin/social.php — форма создания поста
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $text = $_POST["text"] ?? "";
    $image = $_POST["image_url"] ?? "";
    $platforms = $_POST["platforms"] ?? []; // ["vk", "telegram"]
    
    // Сохраняем в БД
    $stmt = $pdo->prepare("INSERT INTO tg_queue (text, image, platforms, created_at) VALUES (?, ?, ?, NOW())");
    $stmt->execute([$text, $image, json_encode($platforms)]);
    
    // Для VK — публикуем сразу
    if (in_array("vk", $platforms)) {
        postToVK($text, $image);
    }
    
    echo "Пост добавлен в очередь ✅";
}
?>

<form method="post">
    <textarea name="text" rows="6" placeholder="Текст поста..."></textarea>
    <input name="image_url" placeholder="URL картинки (необязательно)">
    <label><input type="checkbox" name="platforms[]" value="vk" checked> VK</label>
    <label><input type="checkbox" name="platforms[]" value="telegram" checked> Telegram</label>
    <button type="submit">Опубликовать</button>
</form>

Компонент 2: VK Poster (PHP)

VK API позволяет постить напрямую через wall.post. Нужен access_token сообщества с правами на стену. Получается через VK Admin → Управление → API.

<?php
// SocialPoster.php — публикация в VK
class SocialPoster {
    private string $vkToken;
    private string $vkGroupId;
    private string $vkApiVersion = "5.199";
    
    function __construct() {
        $this->vkToken = getenv("VK_ACCESS_TOKEN");
        $this->vkGroupId = getenv("VK_GROUP_ID");
    }
    
    function postToVK(string $text, string $imageUrl = ""): array {
        $attachments = "";
        
        // Если есть картинка — загружаем
        if ($imageUrl) {
            $photo = $this->uploadPhoto($imageUrl);
            if ($photo) {
                $attachments = "photo{$photo["owner_id"]}_{$photo["id"]}";
            }
        }
        
        // Публикуем пост
        $params = [
            "owner_id" => "-" . $this->vkGroupId, // минус = группа
            "message" => $text . "\n\n#нейросети #автоматизация",
            "attachments" => $attachments,
            "access_token" => $this->vkToken,
            "v" => $this->vkApiVersion,
        ];
        
        $response = file_get_contents(
            "https://api.vk.com/method/wall.post?" . http_build_query($params)
        );
        
        return json_decode($response, true);
    }
    
    private function uploadPhoto(string $imageUrl): ?array {
        // Шаг 1: получаем URL для загрузки
        $server = json_decode(file_get_contents(
            "https://api.vk.com/method/photos.getWallUploadServer?" . http_build_query([
                "group_id" => $this->vkGroupId,
                "access_token" => $this->vkToken,
                "v" => $this->vkApiVersion,
            ])
        ), true);
        
        $uploadUrl = $server["response"]["upload_url"] ?? "";
        if (!$uploadUrl) return null;
        
        // Шаг 2: загружаем фото на сервер VK
        $imageData = file_get_contents($imageUrl);
        $tmpFile = "/tmp/vk_upload_" . uniqid() . ".jpg";
        file_put_contents($tmpFile, $imageData);
        
        $ch = curl_init($uploadUrl);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => ["photo" => new CURLFile($tmpFile)],
            CURLOPT_RETURNTRANSFER => true,
        ]);
        
        $uploadResult = json_decode(curl_exec($ch), true);
        unlink($tmpFile);
        
        // Шаг 3: сохраняем фото на стене
        $save = json_decode(file_get_contents(
            "https://api.vk.com/method/photos.saveWallPhoto?" . http_build_query([
                "group_id" => $this->vkGroupId,
                "photo" => $uploadResult["photo"],
                "server" => $uploadResult["server"],
                "hash" => $uploadResult["hash"],
                "access_token" => $this->vkToken,
                "v" => $this->vkApiVersion,
            ])
        ), true);
        
        return $save["response"][0] ?? null;
    }
}

// Использование
$poster = new SocialPoster();
$result = $poster->postToVK("Новая статья: Генератор бизнес-планов готов!", "https://bbb2.ru/images/bp-cover.png");
echo $result["response"]["post_id"] ? "ОК, post_id={$result["response"]["post_id"]}" : "Ошибка";

Компонент 3: Telegram Queue Worker (Node.js)

Почему Node.js, а не PHP для Telegram? PHP curl блокирует процесс. Node.js с undici работает асинхронно — десятки постов в очереди не блокируют друг друга.

// tg_queue_worker.js — воркер очереди Telegram
import { fetch, Agent } from "undici";
import mysql from "mysql2/promise";

const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
const CHANNEL_ID = process.env.TELEGRAM_CHANNEL_ID;

// IPv4-only Agent (VPS ограничение)
const dispatcher = new Agent({ 
    family: 4,
    connect: { keepAlive: true },
});

const db = await mysql.createConnection({
    host: "localhost",
    user: "play",
    password: process.env.DB_PASSWORD,
    database: "play",
});

async function processQueue() {
    try {
        // Забираем неотправленные посты
        const [rows] = await db.execute(
            "SELECT id, text, image_url FROM tg_queue WHERE sent = 0 AND platforms LIKE '%telegram%' ORDER BY created_at LIMIT 5"
        );

        for (const post of rows) {
            await sendToTelegram(post);
            await db.execute("UPDATE tg_queue SET sent = 1, sent_at = NOW() WHERE id = ?", [post.id]);
            console.log(`✅ Пост #${post.id} отправлен`);
        }
    } catch (err) {
        console.error("Ошибка очереди:", err.message);
    }
}

async function sendToTelegram(post) {
    const url = `https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`;
    
    // Форматируем текст под Telegram HTML
    let text = post.text
        .replace(/<strong>/g, "").replace(/<\/strong>/g, "")
        .replace(/<em>/g, "").replace(/<\/em>/g, "");

    const body = JSON.stringify({
        chat_id: CHANNEL_ID,
        text: text,
        parse_mode: "HTML",
        disable_web_page_preview: false,
    });

    const res = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body,
        dispatcher,
    });

    if (!res.ok) {
        const err = await res.text();
        throw new Error(`HTTP ${res.status}: ${err}`);
    }

    // Если есть картинка — отправляем отдельно
    if (post.image_url) {
        await sendPhoto(post.image_url);
    }
}

async function sendPhoto(imageUrl) {
    const url = `https://api.telegram.org/bot${BOT_TOKEN}/sendPhoto`;
    const body = JSON.stringify({
        chat_id: CHANNEL_ID,
        photo: imageUrl,
    });

    await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body,
        dispatcher,
    });
}

// Запускаем каждые 2 минуты
console.log("🚀 TG Queue Worker запущен (интервал: 2 мин)");
setInterval(processQueue, 120_000);
processQueue(); // первый запуск сразу

Cron: настройка

# /etc/cron.d/bbb2_autoposting
# TG Queue Worker — каждые 2 минуты
*/2 * * * * root NODE_PATH=/usr/lib/node_modules node /var/www/www-root/data/www/play.ru/tg_queue_worker.js >> /var/log/tg_queue.log 2>&1

Важно: нужен NODE_PATH указывающий на undici (из OpenClaw). Без этого Node.js не найдёт модуль.

Форматирование под платформы

VK и Telegram — разная разметка. Мой SocialPoster.php преобразует один текст в два формата:

// Функция форматирования под платформу
function formatForPlatform(string $text, string $platform): string {
    if ($platform === "telegram") {
        // Telegram: HTML-разметка, без таблиц, с эмодзи-маркерами
        $text = str_replace(["<h2>", "</h2>"], ["<b>", "</b>\n"], $text);
        $text = preg_replace("/<table.*?<\/table>/s", "[таблица — смотрите на сайте]", $text);
        return $text;
    } else {
        // VK: чистый текст + хэштеги
        $text = strip_tags($text);
        $text .= "\n\n#нейросети #автоматизация #бизнес";
        return $text;
    }
}

Результаты: деньги и время

ПоказательБыло (ручной)Стало (авто)
Время на пост15-20 мин30 сек (заполнить форму)
Ежемесячные расходы15 000-30 000 ₽ (SMM)0 ₽
Охват ошибокЗабыл опубликовать0 пропусков (cron)
Платформы1-2 (лень)VK + Telegram — всегда

Экономия: 15 000-30 000 ₽ в месяц. 180 000-360 000 ₽ в год.

А главное — я не думаю о постинге. Написал статью → заполнил форму → забыл. Cron всё сделает.

Что можно улучшить

  1. Планировщик дат — отложенный постинг: указываешь дату/время, воркер публикует по расписанию
  2. AI-оптимизация времени — анализ лучшего времени для публикации на основе статистики
  3. Поддержка Яндекс Дзен — статьи в формате Дзен через их API
  4. Генерация контента — Alice LLM пишет пост, ты только проверяешь и публикуешь

Что это значит для вас: Автопостинг — не rocket science. Это PHP для VK, Node.js для Telegram, MySQL для очереди и cron для расписания. ~200 строк кода, 2 вечера работы. Если нужен готовый код или помощь с настройкой — пишите. Берите код выше, адаптируйте под себя, экономьте 15 000+ ₽ в месяц.