Next.js · App Router

Articles for a Next.js site without manual work

Add a route at app/api/seowriter/route.ts, verify the signature there and save the article to your database. After saving call revalidatePath so the page appears right away instead of after the next build.

Publishing to any website is included from the Starter plan.

27
steps in longread pipeline
~5 min
for a standard article
8,000+
words in longread mode
4
social networks auto-posted
Diagram: SEO Writer sends a finished article as a POST request to your site handler

How it works

1

Drop in the route file

app/api/seowriter/route.ts. Node.js runtime is required — signature verification needs the crypto module.

2

Add the key to your environment

SEOWRITER_SECRET from the site settings. Restart the app or container after adding it.

3

Test the connection

In site settings press Test connection, then Send a sample article — it arrives like a real one but does not spend an article from your quota.

Handler for Next.js

The body is read with req.text() before JSON parsing: those exact bytes are signed. The signed string is “timestamp.body”, algorithm HMAC-SHA256. The timestamp inside the signature prevents replay of a captured request: reject anything older than five minutes. The header may carry several signatures separated by commas — that is the 24 hours after a key rotation, and every one of them must be checked.

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

export const runtime = "nodejs";
const SECRET = process.env.SEOWRITER_SECRET!;

export async function POST(req: NextRequest) {
  const raw = await req.text();
  const timestamp = req.headers.get("x-seowriter-timestamp") ?? "";
  const header = req.headers.get("x-seowriter-signature") ?? "";
  const event = req.headers.get("x-seowriter-event") ?? "";

  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return NextResponse.json({ ok: false, error: "stale" }, { status: 401 });
  }

  const expected = Buffer.from(
    createHmac("sha256", SECRET).update(`${timestamp}.${raw}`).digest("hex"),
  );
  const valid = header.split(",").map((p) => p.trim())
    .filter((p) => p.startsWith("v1="))
    .some((p) => {
      const got = Buffer.from(p.slice(3));
      return got.length === expected.length && timingSafeEqual(expected, got);
    });
  if (!valid) return NextResponse.json({ ok: false }, { status: 401 });

  if (event === "ping") return NextResponse.json({ success: true, cms: "Next.js" });

  const { article } = JSON.parse(raw);

  // Ищем по article.id — повторная доставка обновит запись, а не создаст вторую
  await db.article.upsert({
    where: { seowriterId: article.id },
    update: { title: article.h1, html: article.content_html, status: article.status },
    create: {
      seowriterId: article.id, slug: article.slug, title: article.h1,
      html: article.content_html, cover: article.cover?.url ?? null,
    },
  });

  revalidatePath("/blog");
  revalidatePath(`/blog/${article.slug}`);

  // Верните url — от него зависят внутренняя перелинковка и анонсы в соцсетях
  return NextResponse.json({ ok: true, url: `https://site.ru/blog/${article.slug}` });
}
Download the full file

What arrives with every article

Not just HTML: everything a page needs comes as separate fields, so there is no need to parse the text with regular expressions.

article.idPermanent article id. Update your record by it so a repeated delivery does not create a duplicate
article.statusdraft or publish — publish right away or keep as a draft
article.title, h1, slugTitle, H1 and slug
article.content_htmlReady article HTML with structured data inside
article.metaMeta title and description for the title tag and meta description
article.coverCover: URL, alt and whether the link is permanent
article.imagesImages from the text. Each says whether it is hosted by us or by a third party
article.faqQuestion and answer pairs — if you render your own FAQ block instead of parsing HTML
article.json_ldSchema.org markup blocks as separate objects
article.categoryCategory if one is set in site settings

Who webhook delivery is for

🧑‍💻

Developers of custom projects

The site is hand-written or built on a framework, and a ready integration for it does not exist and never will. The handler takes thirty lines and lives in your repository along with the rest of the code.

🏢

Agencies with a mixed client base

One client runs WordPress, another a custom admin panel, a third Next.js. The webhook removes the question “do you support this CMS”: any of them works, and all client sites live in one account.

Headless projects and static sites

Content lives in your own database or a headless CMS while pages are built at deploy time. The handler saves the article and triggers a rebuild, so the publication appears without a manual commit.

By hand versus by webhook

An article can always be moved by hand — the question is what that costs over time.

By handBy webhook
Getting an article onto the siteCopying text, images and meta tags by handArrives ready within seconds after generation
Time per article20–30 minutes of transferring and formattingZero: no human involved
Images and coverDownload, upload, set alt textArrive as links, the cover as a separate field
Structured data and FAQAssemble manually or forget about itReady Schema.org blocks as separate fields
Scheduled publishingSomeone has to be there at the right hourRuns on its own, even at night
If the site was downThe article waits until someone remembers itSix retries over eight hours plus a delivery log

Security

  • Every request is signed with a key known only to you and us.
  • The signature timestamp prevents replaying a captured request: anything older than five minutes is rejected.
  • We send over https only and never follow redirects.
  • The key can be rotated: for 24 hours requests carry both the old and the new signature so your handler can catch up.

Hand the setup to your AI

Paste this prompt into Claude Code, Cursor or Codex inside your own project: it describes the contract without secrets, and the assistant wires the handler in following your codebase conventions.

Prompt for the assistant
My project needs a server-side webhook handler for the SEO Writer service.

Add a POST endpoint at /api/seowriter using the project's current stack, routing and ORM.
Do not create a separate microservice if the project already has a backend.

The request arrives with these headers:
  X-SEOWriter-Event: article.published | article.updated | ping
  X-SEOWriter-Timestamp: unix time in seconds
  X-SEOWriter-Signature: v1=<hex>[,v1=<hex>]
  X-SEOWriter-Delivery: delivery id, identical across retries

The body is JSON:
{ "event": "article.published",
  "article": { "id": "uuid", "status": "draft|publish", "title": "...", "h1": "...", "slug": "...",
               "content_html": "...", "excerpt": "...", "meta": {"title": "...", "description": "..."},
               "cover": {"url": "...", "alt": "..."}, "images": [{"url": "...", "alt": "..."}],
               "faq": [{"question": "...", "answer": "..."}], "json_ld": [...] } }

Implement step by step:
1. Read the body as raw bytes or text BEFORE parsing JSON.
2. Compute HMAC-SHA256 over "{timestamp}.{raw body}" with the key from the SEOWRITER_SECRET environment variable.
3. The signature header may hold several v1= values separated by commas — check EVERY one, any match counts. Use a constant-time comparison, not plain equality.
4. Reject with 401 if the signature does not match or the timestamp differs from now by more than 300 seconds.
5. Answer ping with 200 and JSON {"success": true}.
6. On article.published and article.updated save the article into the project's existing publication model, updating the record by article.id — a repeated delivery must not create a duplicate. Use content_html as the ready page body.
7. Answer with 200 and JSON {"ok": true, "url": "<published article URL>"} — the service needs that URL for internal linking.
8. Answer fast: move heavy processing into a background job.

Constraints:
- never hardcode the secret, read it from the environment;
- never disable signature verification, not even temporarily;
- never answer with a redirect;
- accept POST only.

At the end, tell me the full endpoint URL after deployment and where to add SEOWRITER_SECRET.

FAQ

Static or server rendering — does it matter?

No. With static generation call revalidatePath after saving and the page rebuilds on the fly. With server rendering nothing extra is needed.

Why not parse the body with req.json()?

The signature is computed over the raw bytes. Parsing JSON and re-serialising it changes key order or spacing, and the signature stops matching. First req.text(), then verification, and only then JSON.parse of that same string.

The site is deployed on Vercel — does anything change?

Only one thing: set the Node.js runtime, because the edge environment has no crypto module. Everything else works as is, including revalidation.

What about images?

They arrive as links already hosted on our storage, so you can render them as is. If you prefer to keep files yourself, download them while saving: each entry in images says whether it is hosted by us or by a third party.

What users say

Real results from real people

I used to pay 3–4 copywriters $15 per article and spend 2 days on approvals. Now I upload 20 keywords on Sunday — by Monday all articles are in WordPress. Organic traffic grew 40% in three months.

AK
Alexey K.
Niche electronics blog owner

We manage 12 client sites. Content used to be the bottleneck — not anymore. SEO Writer generates texts with proper H1/H2/H3 structure, fills Yoast and internal links. Clients are happy, we can take on new ones.

MD
Marina D.
SEO specialist, digital agency

We needed review articles for each product category. With SEO Writer we covered 60 queries in one month — with images, links to product pages and meta descriptions. That volume with copywriters would have cost $900.

DG
Dmitry G.
Electronics online store

Pricing

Start free, scale as you grow

Free

$0/mo

  • 3 articles/month
  • 1 site
  • System API keys
  • Topic research: 1 runs/month
Start for free

Starter

$14/mo

1 290 ₽ · billed in RUB

  • 15 articles/month
  • 2 sites
  • System API keys
  • Topic research: 4 runs/month
  • Custom API keys
  • Scheduler
  • Article rewrite for external platforms
Get started
Popular

Pro

$33/mo

2 990 ₽ · billed in RUB

  • 50 articles/month
  • 5 sites
  • System API keys
  • Topic research: 10 runs/month
  • Custom API keys
  • Scheduler
  • Article rewrite for external platforms
  • Cover generation
Get started

Agency

$110/mo

9 990 ₽ · billed in RUB

  • 150 articles/month
  • 20 sites
  • System API keys
  • Topic research: 30 runs/month
  • Custom API keys
  • Scheduler
  • Article rewrite for external platforms
  • Cover generation
  • API access (coming soon)
  • Priority support
Get started

Secure payment via YooKassa · No auto-renewal · 30 days access

See also

What if you run a popular CMS

For these platforms no handler is needed — connecting takes a couple of minutes with a login and a password.

Your site starts filling itself

Setup takes half an hour including the handler. After that articles arrive on schedule and every delivery is visible in your account.

Create a free account →