For a site that is not WordPress or Shopify: a headless or static site, or a CMS AEOptimiz has no connector for. When you publish, AEOptimiz sends the document to an https address you choose, as a signed JSON POST. Your code decides what publishing means.
Connect
Open Settings, then Sites, then CMS, and enter the address. The signing secret is shown once, when you connect. If you lose it, disconnect and connect again to get a new one.
Connecting sends nothing. Send a test delivery sends a ping, with the header x-aeoptimiz-event: ping. Answer it with any 2xx.
The request
Each delivery carries x-aeoptimiz-event: document.publish, a delivery id and a signature. The JSON body has:
status:draftorpublish, the button that was presseddocument: its id, title, address, excerpt, title tag, meta description, focus keyword, and the body as both Markdown and HTML, without its leading headingexternalId: the id you answered with last time, or null the first time
The document id never changes. Key your posts on it and publishing again updates the post instead of duplicating it. A retried delivery keeps its delivery id, so you can ignore one you have already handled.
Verify the signature
Refuse any request whose signature does not verify. The signature header looks like t=1789380000,v1=6f1e...: an HMAC-SHA256, keyed with your signing secret, of the timestamp, a full stop, and the raw request body exactly as received. Verify before parsing the JSON, and reject timestamps more than five minutes from your clock.
import crypto from "node:crypto";
export function verify(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(
(header ?? "").split(",").map((p) => [p.slice(0, p.indexOf("=")), p.slice(p.indexOf("=") + 1)]),
);
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
Your answer
Any 2xx means delivered. Anything else is shown as a failure to the person who pressed Publish, and they can try again. Redirects are not followed, so answer at the exact address you gave. Answer 401 or 403 when the signature does not verify.
You can answer with JSON such as {"id": "post-9", "url": "https://example.com/blog/post", "status": "publish"}. The id comes back as externalId next time. The url matters most: without it the Publish menu cannot link to the live page, and the checks after publishing cannot run.