Автопостинг VK + Telegram: как я построил систему и сэкономил 15 000 ₽ в месяц
Проблема: ручной постинг — прошлый век
До автопостинга мой процесс выглядел так:
- Написал статью на сайте
- Открыл VK → скопировал текст → вставил → прикрепил картинку → опубликовал
- Открыл Telegram → скопировал текст → переформатировал (другая разметка) → опубликовал
- Повторил для каждого нового поста
Это отнимало 15-20 минут на каждый пост. При 3-4 постах в неделю — час тупой рутины. Умножь на год — 50 часов жизни на копипасту.
Альтернативы, которые я рассматривал и отбросил:
| Вариант | Цена | Почему нет |
|---|---|---|
| SMM-менеджер на аутсорсе | 15 000-30 000 ₽/мес | Дорого, медленно, зависимость от человека |
| SMMplanner / Amplifr | 900-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 всё сделает.
Что можно улучшить
- Планировщик дат — отложенный постинг: указываешь дату/время, воркер публикует по расписанию
- AI-оптимизация времени — анализ лучшего времени для публикации на основе статистики
- Поддержка Яндекс Дзен — статьи в формате Дзен через их API
- Генерация контента — Alice LLM пишет пост, ты только проверяешь и публикуешь
Что это значит для вас: Автопостинг — не rocket science. Это PHP для VK, Node.js для Telegram, MySQL для очереди и cron для расписания. ~200 строк кода, 2 вечера работы. Если нужен готовый код или помощь с настройкой — пишите. Берите код выше, адаптируйте под себя, экономьте 15 000+ ₽ в месяц.