Как это работает

После генерации статьи SeoSmith отправляет POST-запрос на URL вашего сайта. В теле запроса — готовая статья: HTML-текст, мета-теги, FAQ, JSON-LD разметка. Ваш сервер проверяет подпись, сохраняет статью и возвращает {"ok": true}.

Автоматически — не вручную

Webhook-коннектор работает без участия пользователя. Как только статья сгенерирована — она уже летит на ваш сайт. Кнопка «Опубликовать» не нужна.

Что приходит в запросе

SeoSmith отправляет POST-запрос с заголовками:

Content-Type: application/json
X-SeoSmith-Signature: sha256=<hmac-hex>
X-SeoSmith-Event: article.published

Тело запроса — JSON следующей структуры:

{
  "event": "article.published",
  "article": {
    "id": 123,
    "title": "Заголовок статьи (H1)",
    "slug": "slug-stati",
    "url": "slug-stati",
    "body": "<p>Готовый HTML-текст статьи...</p>",
    "meta": {
      "title": "SEO-заголовок до 60 символов",
      "description": "Мета-описание до 165 символов",
      "keywords": ["ключевое слово 1", "ключевое слово 2"]
    },
    "faq": [
      { "q": "Вопрос?", "a": "Ответ." }
    ],
    "json_ld": "{...BlogPosting schema...}",
    "image_prompt": "Описание для генерации изображения",
    "alt_text": "Alt-текст для изображения"
  },
  "company": {
    "name": "Название компании",
    "site_url": "https://ваш-сайт.ru"
  }
}
ПолеОписание
article.bodyГотовый HTML — вставляйте в <article> напрямую
article.meta.titleДля тега <title> и og:title
article.meta.descriptionДля <meta name="description">
article.slugURL-путь статьи, например kak-uluchshit-seo
article.json_ldBlogPosting Schema.org — вставляйте в <head>
article.faqМассив вопрос-ответ для FAQPage разметки

Шаг 1: Создать эндпоинт

Добавьте в свой проект POST-роут. Логика одинакова для любого стека:

  1. Прочитать тело запроса как сырой текст (до парсинга JSON)
  2. Вычислить HMAC-SHA256 от тела с ключом SEOSMITH_WEBHOOK_SECRET
  3. Сравнить с подписью из заголовка X-SeoSmith-Signature (без префикса sha256=)
  4. Если не совпадает — вернуть 401
  5. Распарсить JSON и сохранить статью
  6. Вернуть {"ok": true} со статусом 200

Примеры реализации:

Next.js (App Router)

// app/api/seosmith/route.ts
import { createHmac, timingSafeEqual } from "crypto";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const signature = req.headers.get("x-seosmith-signature") ?? "";
  const received = signature.replace("sha256=", "");

  const expected = createHmac("sha256", process.env.SEOSMITH_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest("hex");

  if (!timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const payload = JSON.parse(rawBody);
  if (payload.event !== "article.published") {
    return NextResponse.json({ ok: true, skipped: true });
  }

  const { article } = payload;
  // Сохраните статью в вашу БД или файловую систему
  // await db.articles.create({ slug: article.slug, body: article.body, ... })

  return NextResponse.json({ ok: true });
}

Laravel

// routes/api.php
Route::post('/seosmith', function (Request $request) {
    $rawBody = $request->getContent();
    $signature = $request->header('X-SeoSmith-Signature', '');
    $received = str_replace('sha256=', '', $signature);
    $expected = hash_hmac('sha256', $rawBody, env('SEOSMITH_WEBHOOK_SECRET'));

    if (!hash_equals($expected, $received)) {
        return response()->json(['error' => 'Invalid signature'], 401);
    }

    $payload = $request->json()->all();
    if (($payload['event'] ?? '') !== 'article.published') {
        return response()->json(['ok' => true, 'skipped' => true]);
    }

    $article = $payload['article'];
    // Article::create(['slug' => $article['slug'], 'body' => $article['body'], ...]);

    return response()->json(['ok' => true]);
});

Python (FastAPI)

import hashlib, hmac, os
from fastapi import APIRouter, Header, HTTPException, Request
from fastapi.responses import JSONResponse

router = APIRouter()

@router.post("/api/seosmith")
async def receive_webhook(
    request: Request,
    x_seosmith_signature: str = Header(alias="X-SeoSmith-Signature"),
):
    raw_body = await request.body()
    secret = os.environ["SEOSMITH_WEBHOOK_SECRET"]
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    received = x_seosmith_signature.removeprefix("sha256=")

    if not hmac.compare_digest(expected, received):
        raise HTTPException(status_code=401, detail="Invalid signature")

    payload = await request.json()
    if payload.get("event") != "article.published":
        return JSONResponse({"ok": True, "skipped": True})

    article = payload["article"]
    # Сохраните статью в вашу БД
    return JSONResponse({"ok": True})
Важно: читайте тело как текст до парсинга

HMAC вычисляется от сырого тела запроса, а не от объекта JSON. Если сначала распарсить, а потом сериализовать обратно — порядок ключей может измениться и подпись не совпадёт.

Куда сохранять статью

Это зависит от архитектуры вашего проекта. Универсального ответа нет — выберите подходящий вариант:

Тип проектаКуда сохранятьURL статьи
Next.js / Nuxt с БД Таблица articles или posts в вашей БД /blog/[slug] — динамический роут
Next.js / Nuxt с MDX Файл /content/blog/{slug}.mdx /blog/[slug] — файловый роутинг
Laravel / Django Таблица posts или articles /blog/{slug} — роут в контроллере
Статический сайт (Hugo, Jekyll) Файл /content/posts/{slug}.md /posts/{slug}/ — после пересборки
Headless CMS (Strapi, Directus) API CMS — POST /api/articles Зависит от настроек CMS
Используйте article.slug как идентификатор

Поле article.slug — уникальный URL-совместимый идентификатор статьи на кириллице, транслитерированный в латиницу. Используйте его как первичный ключ или имя файла. Путь до блога (/blog/, /articles/) добавляйте сами — вы знаете структуру своего сайта лучше.

Если в проекте уже есть таблица или папка для публичных страниц — используйте её. Если структура ещё не определена, создайте новую: например таблицу seo_articles со столбцами slug, title, body, meta_title, meta_description, created_at.

Шаг 2: Задеплоить изменения

Опубликуйте код на сервер. Эндпоинт должен быть доступен по HTTPS — SeoSmith не принимает HTTP-адреса.

Проверьте что эндпоинт отвечает (должен вернуть 401 — подпись не передана, но эндпоинт живой):

curl -X POST https://ваш-сайт.ru/api/seosmith

Шаг 3: Сохранить коннектор в SeoSmith и получить секрет

  1. Откройте SeoSmith → нужный проект → Интеграции
  2. Выберите карточку Webhook — свой сайт или Вайбкод
  3. Вставьте URL эндпоинта: https://ваш-сайт.ru/api/seosmith
  4. Нажмите Сохранить
  5. Появится Шаг 3 с секретом — скопируйте его сразу, он показывается только один раз
Секрет показывается один раз

После закрытия секрет нельзя восстановить. Если потеряли — удалите коннектор и создайте заново, получите новый секрет.

Шаг 4: Добавить секрет в .env

Добавьте секрет в файл .env вашего проекта — и локально, и на сервере:

SEOSMITH_WEBHOOK_SECRET=вставьте-секрет-здесь

После добавления на сервере обязательно перезапустите приложение — переменные окружения читаются только при старте:

СтекКоманда перезапуска
Docker Composedocker compose up -d backend
PM2 (Node.js)pm2 restart all
Systemdsudo systemctl restart your-app
Vercel / RailwayДобавить через панель Environment Variables — деплой автоматический
Herokuheroku config:set SEOSMITH_WEBHOOK_SECRET=значение

Шаг 5: Проверить подключение

Нажмите «Проверить» в карточке коннектора. SeoSmith отправит тестовый запрос с событием test — ваш эндпоинт должен вернуть 200.

Если всё прошло успешно — статус сменится на «Подключено». Теперь каждая новая статья будет автоматически отправляться на ваш сайт.

Решение типичных проблем

401 — Invalid signature

Значения секрета в SeoSmith и в .env на сервере не совпадают, или приложение не было перезапущено после добавления секрета. Проверьте оба варианта.

500 — Webhook secret not configured

Переменная SEOSMITH_WEBHOOK_SECRET не добавлена в .env на сервере, или приложение не было перезапущено. Добавьте переменную и перезапустите.

Тест проходит, но статьи не приходят

Тестовый запрос содержит event: "test". Убедитесь, что ваш эндпоинт обрабатывает event: "article.published" и не игнорирует его. Проверьте логи приложения в момент генерации статьи.

Эндпоинт недоступен по HTTPS

SeoSmith принимает только HTTPS-адреса. Если у вас нет SSL-сертификата — получите бесплатный через Let's Encrypt (certbot) или разверните проект на платформе с автоматическим SSL (Vercel, Railway, Render).