Help

Connect a Next.js website

For websites built with Next.js — your developer adds one route, about 30 minutes.

Last updated October 13, 2026

How it works

Next.js has no built-in blog editor, so StrideRank sends every finished article to a route in your app. The request is signed with a secret only you and StrideRank know. Your route checks the signature, saves the article wherever your site keeps its posts — a CMS, a database or files — and answers with the post’s ID and public address. The full format is described in Connect your own system (webhook).

  • The route must be reachable over https:// on port 443 from the internet. Local and private addresses are refused.
  • Redirects are not followed — enter the final address.
  • The route has 20 seconds to answer.

Set it up

  1. Add the two files

    Copy verify.js and route.ts below into app/api/articles/ (any path works). Replace savePost with your own code that creates the post, or updates it when article.externalId is set.

  2. Deploy and enter the address

    In StrideRank, open Autopilot → Settings and go to Publishing. During setup, the step-by-step guide on the connect step does the same. Only owners and admins of the account can change the connection. Choose Webhook (JSON) and enter the full address of the route, for example https://example.com/api/articles.

  3. Save the signing secret

    Click Save connection. StrideRank shows the signing secret only once — store it as the environment variable WEBHOOK_SIGNING_SECRET and redeploy.

  4. Test the connection

    Click Test connection. We send a ping; the route answers with status 200.

The code

app/api/articles/verify.js
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 300; // reject requests older than 5 minutes

// rawBody: the request body exactly as received (a Buffer), before JSON.parse
export function verifySignature(rawBody, header, secret) {
  if (typeof header !== 'string') return false;
  const parts = Object.fromEntries(
    header.split(',').map((part) => part.trim().split('=', 2)),
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !/^[0-9a-f]{64}$/.test(parts.v1 ?? '')) return false;
  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;

  const expected = createHmac('sha256', secret)
    .update(`${t}.`)
    .update(rawBody)
    .digest();
  const received = Buffer.from(parts.v1, 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}
app/api/articles/route.ts
import { verifySignature } from './verify';
import { savePost } from '@/lib/posts'; // your own code: create or update the post

export const runtime = 'nodejs'; // node:crypto is needed for the signature

export async function POST(request: Request) {
  // Read the raw body first: the signature is made over these exact bytes.
  const raw = Buffer.from(await request.arrayBuffer());
  const signature = request.headers.get('x-seo-autopilot-signature');
  if (!verifySignature(raw, signature, process.env.WEBHOOK_SIGNING_SECRET)) {
    return new Response(null, { status: 401 });
  }
  const body = JSON.parse(raw.toString('utf8'));

  if (body.event === 'ping') {
    return Response.json({ ok: true });
  }
  if (body.event === 'article.publish') {
    // body.article.externalId is set when we update an article you published before.
    const post = await savePost(body.article);
    return Response.json({ id: String(post.id), url: post.url });
  }
  return new Response(null, { status: 400 });
}

Read the body with request.arrayBuffer() before anything else. The signature is made over the raw bytes; request.json() would change them.

The route needs the Node.js runtime (node:crypto). Don’t move it to the Edge runtime.

Questions

Do I need the Pages Router version?

The same logic works in a Pages Router API route. Turn off the body parser for that route so you can read the raw body for the signature check.