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

После генерации статьи 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",
    "short_answer": "Короткий ответ по теме статьи",
    "insights": [
      "Первый ключевой тезис",
      "Второй ключевой тезис",
      "Третий ключевой тезис",
      "Четвёртый ключевой тезис"
    ],
    "body_html": "

Заголовок раздела

Основной текст статьи в HTML...

", "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.idВнешний идентификатор статьи. Используйте его как основной ключ для обновления уже существующей публикации
article.short_answerКороткий ответ по теме статьи. Можно выводить отдельным блоком в начале страницы
article.insightsМассив ключевых тезисов. Подходит для блока «Главное из статьи»
article.body_htmlГотовый HTML-фрагмент статьи. Для публикации на сайте используйте именно его
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 и создать или обновить статью по article.id или article.slug
  6. Для публичной страницы использовать article.body_html как готовый HTML статьи
  7. Вернуть {"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;
  // Сохраните или обновите статью по article.id или article.slug
  // body обычно приходит в markdown и должен быть отрендерен в HTML
  // await db.articles.upsert({ externalId: article.id, slug: article.slug, body_html: article.body_html, short_answer: article.short_answer, insights: article.insights, ... })

  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::updateOrCreate(['external_id' => $article['id']], ['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.id и article.slug как ключи upsert

article.id удобен как внешний идентификатор, а article.slug — как резервный URL-совместимый ключ. Если статья пришла повторно после правок в SeoSmith, обновляйте существующую запись и карточку в списке блога, а не создавайте новую.

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

Что делать при повторном webhook после правки статьи

Если пользователь в SeoSmith доработал текст, SeoSmith может повторно отправить ту же статью. Это не новая публикация, а новая версия существующей.

  • Найдите публикацию по article.id, а если такого поля нет в модели проекта — по article.slug.
  • Обновите текст, body_html, short_answer, insights, meta-поля и FAQ.
  • Пересоберите страницу статьи или обновите запись в CMS.
  • Если у проекта есть страница раздела блога, обновите карточку статьи в её источнике данных вместо создания дубликата.

Универсальный prompt для разработчика или AI

Если endpoint будет писать не человек, а AI-ассистент или внешний разработчик, можно дать ему такой контракт:

Добавь в проект POST-endpoint /api/seosmith для webhook SeoSmith.

Требования:
- читать raw body до JSON.parse;
- проверять HMAC-SHA256 подпись из X-SeoSmith-Signature по SEOSMITH_WEBHOOK_SECRET;
- принимать payload с event, article и company;
- если event !== article.published, возвращать { ok: true, skipped: true };
- если event === article.published, сохранять или обновлять статью по article.id или article.slug;
- не создавать дубль, если webhook пришёл повторно после правки статьи;
- сохранять title, slug, url, short_answer, insights, body_html, meta.title, meta.description, meta.keywords, faq, json_ld;
- для публичной страницы использовать body_html как готовый HTML статьи;
- обеспечить, чтобы статья открывалась по публичному URL и автоматически попадала в страницу раздела блога;
- использовать уже существующие стили, шаблоны и скрипты проекта для статьи и карточки блога.

В конце покажи:
1. Полный URL endpoint.
2. Где теперь хранится статья.
3. Как обновляется страница раздела блога.
4. По какому правилу происходит update существующей статьи вместо создания дубля.

Шаг 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).