OXIRANKER DEVELOPER DOCS

Documentația OxiRanker

Un centru de documentație clar pentru conectarea site-ului tău la serviciile OxiRanker. Începe cu integrarea WordPress sau integrarea API, apoi urmează ghidul pas cu pas.

Documentație oficialăFull-feature V1Pentru proprietari de site-uri, dezvoltatori și echipe de integrare
Meniu documentație
Alege un ghid din meniu. Această pagină este centrul principal de documentație pentru OxiRanker.
Serviciu API

Serviciu API

Folosește Export API și Webhook pentru site-uri personalizate, platforme CMS personalizate, Next.js, Laravel, Node.js, PHP sau backenduri Python.

Domeniu
Verificat
Conținut
Gata
API
Webhook
Notificare importantă despre stocare

OxiRanker nu este hosting permanent pentru site-ul de destinație

OxiRanker nu oferă garanție de hosting permanent pentru conținutul, imaginile sau fișierele exportate ale site-ului de destinație. OxiRanker nu este un serviciu de hosting pentru conținutul utilizatorilor, iar conținutul generat, imaginile de copertă, imaginile Open Graph sau fișierele de output pot să nu rămână pe infrastructura OxiRanker mai mult de 90 de zile.

Prin urmare, după primirea outputului, site-ul de destinație trebuie să stocheze articolul final, imaginile, datele SEO, datele Open Graph și datele Schema în propria bază de date, pe propriul server, în propriul storage sau CDN și nu trebuie să se bazeze pe URL-urile OxiRanker pentru afișarea publică.

Video de instruire

Video de instruire pentru conectare

Folosește acest video ca ghid vizual înainte de conectarea WordPress sau implementarea integrării API.

API Output

Implementează fluxul de output pas cu pas

Stochează mai întâi Export Token și Webhook Secret în backendul tău, apoi finalizează Webhook receiver, baza de date, logica UPSERT, Image Mirroring și syncul zilnic.

PASUL 01

Înregistrează-te și creează un domeniu în OxiRanker

Mai întâi, înregistrează-te în OxiRanker, autentifică-te în cont și apasă Add Domain din secțiunea Domains.

Format corect al domeniului

În câmpul Domain name, introdu doar domeniul rădăcină. Adresa nu trebuie să includă http, https sau www.

example.com

Formate greșite

Nu introduce aceste formate în câmpul domeniului.

https://example.com
http://example.com
www.example.com
După crearea domeniului
Dacă formatul domeniului este valid, OxiRanker creează domeniul și te duce la pasul de verificare a proprietății domeniului.
PASUL 02

Verifică proprietatea domeniului

În acest pas, trebuie să dovedești că domeniul îți aparține sau că ai acces pentru administrarea lui.

DNS TXT

Recomandat

Adaugă un TXT record în DNS-ul domeniului. Pentru Cloudflare, aceasta este de obicei cea mai simplă metodă.

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

Plasează un fișier text în pathul .well-known al site-ului tău, astfel încât OxiRanker să îl poată verifica.

File URL:
https://example.com/.well-known/yourdomain-verification.txt

File Content:
220daaa37****42730f27

Meta Tag

Plasează un meta tag în secțiunea head a homepage-ului site-ului tău.

<meta name="yourdomain-verification" content="190208d****d275b65" />
Notă DNS importantă
După adăugarea DNS recordului, mai ales în Cloudflare, poate fi nevoie să aștepți 2 până la 3 minute. Apoi apasă Confirm pe aceeași pagină OxiRanker.
Durata de valabilitate a tokenului de verificare
Valorile de verificare ale domeniului au o limită de timp. În prezent, valorile de verificare sunt de obicei valide timp de 1440 de minute.
PASUL 03

Primește tokenuri gratuite după verificarea primului domeniu

După verificarea primului domeniu, OxiRanker adaugă 1000 de tokenuri gratuite în contul tău pentru testare inițială.

Doar pentru primul domeniu
Acest cadou se adaugă doar pentru primul domeniu verificat și nu se adaugă din nou pentru domeniile următoare.
PASUL 04

Configurează limbile și Blog URL

În secțiunea Blog URLs, definește dacă site-ul tău este într-o singură limbă sau multilingv și ce limbi trebuie folosite pentru articolele generate.

Alege limbile

OxiRanker acceptă 24 de limbi. Poți alege una sau mai multe limbi în funcție de structura site-ului tău.

Introdu Blog URL

Pentru fiecare limbă, introdu blog URL-ul acelei limbi. Dacă site-ul tău este multilingv, introdu separat pathul fiecărei limbi.

https://example.com/fa/blog
https://example.com/en/blog
Structură URL
Dacă site-ul tău are o structură diferită, introdu blog URL-ul real al site-ului tău. Punctul important este ca URL-ul final să fie pathul unde articolele trebuie publicate.
PASUL 05

Adaugă cuvinte-cheie pentru fiecare limbă

În tabul Keywords, adaugă cel puțin 3 și până la 50 de cuvinte-cheie pentru fiecare limbă.

Regula principală

Cuvintele-cheie pentru fiecare limbă trebuie introduse în aceeași limbă. Dacă limba articolului este engleza, cuvintele-cheie trebuie să fie tot în engleză.

Cum adaugi cuvinte-cheie

După ce tastezi fiecare cuvânt-cheie, apasă Enter pentru a-l adăuga. După completarea tuturor cuvintelor-cheie, apasă Save.

Greșeală comună
Dacă introduci cuvinte-cheie turcești pentru limba engleză, pot apărea expresii turcești în conținutul englez. Limba cuvintelor-cheie trebuie să corespundă cu limba articolului.
PASUL 06

Completează Article Profile al domeniului

Article Profile îi spune OxiRanker ce este brandul tău, pentru cine scrie, ce ton trebuie să folosească și ce subiecte trebuie să evite.

Industrie

Definește piața sau categoria de business. Exemplu: consultanță imobiliară și cumpărarea și vânzarea proprietăților în Dubai.

Descrierea businessului

Explică ce vinde businessul, pe cine servește, cum funcționează și ce îl face diferit.

Audiență

Definește cui trebuie să se adreseze conținutul. Fiecare audiență poate fi salvată separat.

Ton

Controlează stilul și vocea conținutului generat, cum ar fi formal, educațional, simplu, tehnic sau orientat spre vânzări.

Obiectivul conținutului

Definește ce trebuie să obțină articolele, cum ar fi atragerea clienților, creșterea traficului sau educarea utilizatorilor.

Subiecte interzise

Orice lucru despre care brandul nu ar trebui să vorbească sau cu care nu ar trebui asociat se introduce aici.

CTA opțional

CTA înseamnă acțiunea pe care vrei ca cititorul să o facă după citirea articolului. Dacă nu ești sigur ce text este potrivit, este mai bine să lași acest câmp gol.

Profil multilingv
Dacă site-ul tău este multilingv, trebuie să completezi profilul doar într-o singură limbă. OxiRanker poate pregăti versiunile necesare pentru alte limbi pe baza aceluiași profil.
PASUL 07

Linkuri interne și externe

OxiRanker poate aplica inteligent linkuri interne și externe în interiorul articolelor.

Linkuri interne

Această opțiune este activată implicit și este mai bine să rămână activată. Linkurile interne ajută motoarele de căutare să înțeleagă mai bine structura conținutului site-ului tău.

În primul articol, linkurile interne de obicei nu sunt create deoarece nu există încă un articol anterior. De la al doilea articol înainte, sistemul poate face linking intern.

Linkuri externe

Linkurile externe sunt adăugate către surse oficiale și de încredere și sunt marcate ca nofollow, astfel încât autoritatea SEO să nu fie transferată neintenționat.

PASUL 08

Verifică calitatea profilului cu AI

După completarea Article Profile, OxiRanker verifică calitatea profilului.

85+
Scor minim necesar pentru salvare
95
Scor excelent atins de obicei după îmbunătățiri
AI
Sugestii profesionale pentru fiecare câmp
Dacă scorul este sub 85
Sistemul nu permite salvarea finală și afișează explicații și sugestii de îmbunătățire pentru fiecare câmp problematic. Poți aplica sugestiile și salva din nou.
PASUL 09

Conectează Telegram pentru rezumatele articolelor

După salvarea cu succes a profilului, poți activa trimiterea rezumatelor articolelor către un canal Telegram.

1

Adaugă botul

Adaugă @Oxiranker_Bot în canalul tău Telegram și fă-l Admin.

2

Introdu username-ul canalului

Introdu numele canalului cu @. Exemplu: @my_channel

3

Activează livrarea rezumatului

După înregistrarea unui canal valid, activează livrarea rezumatului articolului.

Beneficiu Telegram
Dacă este activat, după generarea conținutului, rezumatul articolului este trimis pe canalul Telegram împreună cu imaginea și linkul. Acest lucru poate consolida rutele de trafic către site-ul tău.
PASUL 10

Configurează motorul automat de articole

În tabul Automatic article generation, poți gestiona motorul de generare automată a articolelor.

Selecție inteligentă a subiectului

Sistemul folosește cuvinte-cheie și Article Profile pentru a selecta subiecte potrivite, unice și publicabile.

Generare conștientă de brand

Tonul, audiența, obiectivul conținutului și restricțiile brandului sunt respectate în timpul generării articolului.

Publicare programată

Capacitatea lunară este distribuită de-a lungul lunii, astfel încât publicarea conținutului să pară mai naturală și consistentă.

Capacitate lunară

7 articles / month
10 articles / month
15 articles / month
30 articles / month

30 de articole înseamnă aproape un articol pe zi. 15 articole înseamnă aproape o dată la două zile. 10 articole înseamnă aproximativ la fiecare două-trei zile. 7 articole înseamnă aproximativ la fiecare trei-patru zile.

Lungimea articolului și imaginea

Lungimea articolului poate fi Short, Standard sau Long. Standard este o alegere echilibrată pentru majoritatea site-urilor.

Imaginile articolelor pot fi activate sau dezactivate, iar tema imaginii poate fi Light sau Dark.

Notă pentru site-uri API
Motorul automat generează articole în interiorul OxiRanker. Pentru a muta fiecare articol pe site-ul tău personalizat după generare, trebuie să implementezi Export API, Webhook, stocare în baza de date și Image Mirroring.
PASUL 11

Creează primul articol din Generate

După pregătirea domeniului, deschide meniul Generate, alege domeniul și apasă New Content sau New Article.

1

Limbă

Pentru fiecare cerere de conținut se selectează o singură limbă.

2

Subiect

Scrie un subiect clar. Source URL este opțional.

3

Imagine

Activează sau dezactivează imaginea și alege tema Light sau Dark.

4

Plată și generare

Costul este dedus din wallet și începe generarea articolului.

Timp de generare
Generarea articolului și a imaginii durează de obicei aproximativ 2 până la 3 minute. După finalizare, articolul este vizibil în OxiRanker.
Source URL
Dacă Source URL este introdus, OxiRanker îl folosește pentru a înțelege mai bine subiectul, dar nu generează conținut copiat. Sistemul este conceput pentru a crea conținut unic.
PASUL 12

Verifică Preview, SEO Report și JSON Output

După generarea articolului, sunt disponibile mai multe outputuri importante pentru verificarea calității și pregătirea publicării.

Preview

Afișează textul articolului și imaginea generată. Această secțiune este utilă pentru verificarea calității finale înainte de publicare.

SEO Report

Afișează un raport SEO ușor de înțeles, inclusiv SEO Score, title, meta description, canonical, keywords, schema, headings, links, images și OpenGraph.

JSON Output

Afișează outputul complet al articolului pentru utilizatori tehnici, inclusiv slug, html_body, schema, URL-uri de imagini, tags, category, timp de citire și timestampuri created/updated.

Semnificația outputurilor
Aceste outputuri arată că articolul nu este doar text simplu. Este conținut structurat, pregătit pentru SEO, pregătit pentru WordPress, CMS, API și sisteme de publicare.
PASUL 13

Ce fac exact Export API și Webhook?

După ce un articol este generat în OxiRanker, site-ul de destinație îl poate citi cu Export API sau poate fi notificat prin Webhook atunci când articolul este gata.

Metoda 1: Export API

Site-ul de destinație poate citi articole din OxiRanker cu Export Token ori de câte ori este necesar. Această metodă este folosită pentru sync manual, sync zilnic, pagini de listă a articolelor și fetch pentru JSON-ul complet al articolului.

GET https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Metoda 2: Webhook

Când este creat un articol nou sau un articol este actualizat, OxiRanker notifică endpointul site-ului de destinație. Webhook nu trimite articolul complet. Trimite doar identificatorul articolului, astfel încât site-ul de destinație să poată face fetch pentru articolul complet din Export JSON API.

https://example.com/api/oxiranker/webhook
{
  "event": "article.created",
  "domain_id": "1",
  "article_id": "5",
  "request_id": "5",
  "lang": "fa",
  "slug": "smart-internal-links",
  "occurred_at": "2026-05-20T19:35:33.000Z"
}
Notă foarte importantă
Webhook este doar un mesaj lightweight. Site-ul de destinație nu trebuie să se aștepte la articolul complet în interiorul Webhook. După primirea domain_id și article_id, trebuie să facă fetch pentru articolul complet din Export JSON API și să îl stocheze în propria bază de date.
PASUL 14

Responsabilitatea stocării conținutului și Image Mirroring

Site-ul de destinație trebuie să stocheze articolul final și imaginile pe propria infrastructură.

Ce trebuie să stocheze site-ul de destinație

Textul complet al articolului în baza de date a site-ului de destinație
Imaginea principală a articolului pe serverul, storage-ul sau CDN-ul de destinație
Imagine Open Graph pe serverul, storage-ul sau CDN-ul de destinație
Date SEO, OpenGraph și Article Schema
raw_detail_payload și raw_list_item pentru păstrarea outputului original

Exemplu corect de path imagine

Folosirea directă și permanentă a URL-urilor imaginilor OxiRanker nu este corectă. Imaginea trebuie descărcată, iar URL-urile articolului trebuie înlocuite cu URL-uri interne ale site-ului de destinație.

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker nu este hosting permanent pentru site-ul de destinație
OxiRanker nu este responsabil pentru stocarea permanentă a conținutului de output și a imaginilor pentru site-ul de destinație. OxiRanker nu este un serviciu de hosting pentru conținutul utilizatorilor, iar conținutul generat, imaginile de copertă, imaginile Open Graph sau fișierele de output pot să nu rămână pe infrastructura OxiRanker mai mult de 90 de zile. Prin urmare, site-ul de destinație trebuie să stocheze articolul final, imaginile, datele SEO, OpenGraph și Schema în propria bază de date, pe propriul server, storage sau CDN.
PASUL 15

Obține Export Token și endpointuri gata

Export Token și endpointurile gata de utilizare sunt disponibile din pagina articolului, tabul Outputs și secțiunea API Output.

De unde se obține Export Token

Creează primul articol în OxiRanker.
Deschide pagina acelui articol.
Deschide tabul Outputs.
Deschide secțiunea API Output.
Creează un token din Token management.
Tokenul complet este afișat o singură dată.
Stochează tokenul în backendul site-ului de destinație sau în .env.
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}

Endpointuri gata de utilizare

Pe aceeași pagină a articolului, în secțiunea API Output, Ready-to-use endpoints afișează URL-uri pregătite.

https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=20&offset=0
https://oxiranker.com/api/v1/users/domains/1/articles/1/export/json
https://oxiranker.com/api/v1/users/domains/1/articles/1/export/html?canonical_mode=absolute&include_css=0
Rotirea Tokenului
Dacă Export Token este rotit în panoul OxiRanker, tokenul anterior expiră. Noua valoare trebuie să înlocuiască imediat vechea valoare în .env sau în mediul backend al site-ului de destinație, iar backendul de destinație trebuie repornit sau redeployat.
PASUL 16

Creează Webhook Secret și endpointul de destinație

Webhook Secret este creat tot din pagina articolului, tabul Outputs și secțiunea API Output.

Unde se creează Webhook Secret?

Deschide pagina articolului.
Deschide tabul Outputs.
Deschide secțiunea API Output.
Creează un secret în secțiunea Webhook Secret.
Secretul complet este afișat o singură dată.
Stochează secretul în .env al site-ului de destinație.

Endpoint fix de destinație

OxiRanker acceptă doar următorul path fix pentru Webhook de destinație. Site-ul de destinație trebuie să creeze acest endpoint în backendul său.

POST /api/oxiranker/webhook

https://example.com/api/oxiranker/webhook

Acest endpoint trebuie să accepte doar POST, să păstreze raw body, să verifice timestamp și signature și să returneze un răspuns de succes pentru webhook.test.

Evenimente active
În versiunea curentă, evenimentele sunt fixe: article.created și article.updated. Pentru testarea conexiunii, se trimite valoarea event webhook.test.
PASUL 17

Valori .env necesare pentru site-ul de destinație

OxiRanker nu are acces la fișierele site-ului de destinație sau la .env. Dezvoltatorul site-ului de destinație trebuie să stocheze aceste valori în propriul backend.

OXIRANKER_EXPORT_API_BASE_URL=https://oxiranker.com
OXIRANKER_EXPORT_API_TOKEN=YOUR_EXPORT_TOKEN
OXIRANKER_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET
OXIRANKER_WEBHOOK_MAX_SKEW_MS=300000
OXIRANKER_SOURCE_PROVIDER=oxiranker
OXIRANKER_SOURCE_DOMAIN_IDS=1

SITE_ARTICLE_IMAGE_UPLOAD_DIR=/var/www/example.com/uploads/site-articles
SITE_ARTICLE_IMAGE_MAX_BYTES=10485760

PUBLIC_BASE_URL=https://example.com

OXIRANKER_SYNC_ENABLED=true
OXIRANKER_SYNC_HOUR=3
OXIRANKER_SYNC_MINUTE=10
OXIRANKER_SYNC_PAGE_LIMIT=20
OXIRANKER_SYNC_MAX_PAGES=50

Valori de securitate

OXIRANKER_EXPORT_API_TOKEN
Tokenul Export API trebuie stocat doar în backend.
OXIRANKER_WEBHOOK_SECRET
Webhook Secret trebuie stocat doar în backend.
OXIRANKER_WEBHOOK_MAX_SKEW_MS
Diferența maximă de timp permisă pentru Webhook. 300000 înseamnă 5 minute.

Valori de stocare

SITE_ARTICLE_IMAGE_UPLOAD_DIR
Pathul fizic pentru stocarea imaginilor mirrorate pe serverul de destinație.
SITE_ARTICLE_IMAGE_MAX_BYTES
Dimensiunea maximă permisă a imaginii pentru descărcare și stocare.
PUBLIC_BASE_URL
URL-ul public al site-ului de destinație folosit pentru a crea URL-urile finale ale imaginilor și articolelor.
Securitate Frontend
Export Token și Webhook Secret nu trebuie plasate în cod frontend, JavaScript vizibil utilizatorului, localStorage sau obiectul window.
PASUL 18

Export API: List, JSON, HTML și ZIP

Site-ul de destinație poate folosi Export API pentru a primi lista articolelor sau outputul complet al fiecărui articol.

Obține lista articolelor pentru un domeniu

GET https://oxiranker.com/api/v1/users/domains/:domainId/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

limit definește numărul de articole și este în prezent limitat între 1 și 50. offset este folosit pentru paginație. lang este opțional și este folosit pentru a face fetch articolelor într-o limbă specifică precum fa, en sau tr.

Obține articolul complet ca JSON

GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Acesta este cel mai important output pentru site-ul de destinație. Site-ul de destinație trebuie să stocheze JSON-ul complet al articolului în propria bază de date și să citească din propria bază de date pentru afișarea publică.

Obține HTML gata

GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/html?canonical_mode=absolute&include_css=0
Authorization: Bearer YOUR_EXPORT_TOKEN

HTML este potrivit pentru site-uri care doresc output gata, dar abordarea mai profesională este stocarea JSON și renderizarea lui cu propriul template al site-ului de destinație.

Obține output ZIP

GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/zip
Authorization: Bearer YOUR_EXPORT_TOKEN

Outputul ZIP include article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt și README.txt.

PASUL 19

Webhook Signature și primirea sigură a Webhook

Site-ul de destinație trebuie să verifice semnătura Webhook folosind raw body original.

Webhook headers

content-type: application/json
user-agent: Oxiranker-Webhooks/1.0
x-oxiranker-event: article.created
x-oxiranker-domain-id: 1
x-oxiranker-timestamp: 2026-05-20T19:35:33.000Z
x-oxiranker-signature: sha256=...

Formula semnăturii

payload_to_sign = timestamp + "." + rawBody
signature = "sha256=" + HMAC_SHA256(webhook_secret, payload_to_sign)

Dacă site-ul de destinație parsează mai întâi JSON și apoi îl stringify din nou, ordinea sau spațiile se pot schimba și semnătura poate deveni invalidă. Raw body original trebuie folosit pentru verificarea semnăturii.

Comparație timing-safe
Nu compara semnăturile cu expected === received. În Node.js, este mai bine să folosești crypto.timingSafeEqual.
PASUL 20

Exemplu simplu de cod Node.js pentru primirea Webhook

Acest exemplu Express gestionează raw body, timestamp, signature, webhook.test și evenimentele articolului.

import express from "express";
import crypto from "crypto";

const app = express();

app.use(
  express.json({
    verify: (req, _res, buf) => {
      (req as any).rawBody = Buffer.from(buf);
    },
  })
);

function getHeader(req: express.Request, name: string): string {
  const value = req.headers[name.toLowerCase()];
  if (Array.isArray(value)) return String(value[0] || "").trim();
  return String(value || "").trim();
}

function getRawBody(req: express.Request): string {
  const raw = (req as any).rawBody;
  if (Buffer.isBuffer(raw)) return raw.toString("utf8");
  return JSON.stringify(req.body || {});
}

function createSignature(secret: string, timestamp: string, rawBody: string): string {
  const payload = `${timestamp}.${rawBody}`;
  const digest = crypto
    .createHmac("sha256", secret)
    .update(payload, "utf8")
    .digest("hex");

  return `sha256=${digest}`;
}

function timingSafeEqualText(a: string, b: string): boolean {
  const ab = Buffer.from(a, "utf8");
  const bb = Buffer.from(b, "utf8");

  if (ab.length !== bb.length) return false;
  return crypto.timingSafeEqual(ab, bb);
}

function isTimestampValid(timestamp: string): boolean {
  const maxSkewMs = Number(process.env.OXIRANKER_WEBHOOK_MAX_SKEW_MS || 300000);
  const date = new Date(timestamp);

  if (Number.isNaN(date.getTime())) return false;

  const diff = Math.abs(Date.now() - date.getTime());
  return diff <= maxSkewMs;
}

app.post("/api/oxiranker/webhook", async (req, res) => {
  const secret = String(process.env.OXIRANKER_WEBHOOK_SECRET || "").trim();

  if (!secret) {
    return res.status(500).json({
      error: "webhook_secret_missing",
      message: "Webhook secret is not configured.",
    });
  }

  const eventHeader = getHeader(req, "x-oxiranker-event");
  const domainIdHeader = getHeader(req, "x-oxiranker-domain-id");
  const timestamp = getHeader(req, "x-oxiranker-timestamp");
  const receivedSignature = getHeader(req, "x-oxiranker-signature");
  const rawBody = getRawBody(req);

  if (!eventHeader) {
    return res.status(400).json({
      error: "missing_webhook_event",
      message: "Webhook event header is required.",
    });
  }

  if (!domainIdHeader) {
    return res.status(400).json({
      error: "missing_webhook_domain_id",
      message: "Webhook domain id header is required.",
    });
  }

  if (!timestamp) {
    return res.status(400).json({
      error: "missing_webhook_timestamp",
      message: "Webhook timestamp header is required.",
    });
  }

  if (!receivedSignature) {
    return res.status(400).json({
      error: "missing_webhook_signature",
      message: "Webhook signature header is required.",
    });
  }

  if (!isTimestampValid(timestamp)) {
    return res.status(401).json({
      error: "invalid_webhook_timestamp",
      message: "Webhook timestamp is expired or invalid.",
    });
  }

  const expectedSignature = createSignature(secret, timestamp, rawBody);

  if (!timingSafeEqualText(expectedSignature, receivedSignature)) {
    return res.status(401).json({
      error: "invalid_webhook_signature",
      message: "Invalid webhook signature.",
    });
  }

  const payload = req.body || {};
  const event = String(payload.event || eventHeader || "").trim();

  if (event === "webhook.test") {
    const sourceDomainId = Number(payload.domain_id || domainIdHeader);

    return res.status(200).json({
      ok: true,
      message: "Webhook test received successfully.",
      event,
      source_domain_id:
        Number.isFinite(sourceDomainId) && sourceDomainId > 0 ? sourceDomainId : null,
      test: true,
    });
  }

  if (event !== "article.created" && event !== "article.updated") {
    return res.status(400).json({
      error: "unsupported_webhook_event",
      message: "Webhook event is not supported.",
    });
  }

  const sourceDomainId = Number(payload.domain_id || domainIdHeader);
  const sourceArticleId = Number(payload.article_id);

  if (!Number.isFinite(sourceDomainId) || sourceDomainId <= 0) {
    return res.status(400).json({
      error: "invalid_domain_id",
      message: "Webhook domain id is invalid.",
    });
  }

  if (!Number.isFinite(sourceArticleId) || sourceArticleId <= 0) {
    return res.status(400).json({
      error: "invalid_article_id",
      message: "Webhook article id is invalid.",
    });
  }

  // Fetch the full article from Export API and store it in the database here.
  // await syncArticleFromOxiranker(sourceDomainId, sourceArticleId);

  return res.status(200).json({
    ok: true,
    message: "Webhook processed successfully.",
    event,
    source_domain_id: sourceDomainId,
    source_article_id: sourceArticleId,
  });
});
PASUL 21

Bază de date recomandată pentru site-ul de destinație

Site-ul de destinație trebuie să stocheze articolele în propria bază de date și să prevină duplicatele cu un unique constraint.

CREATE TABLE public.site_articles (
  id BIGSERIAL PRIMARY KEY,

  source_provider VARCHAR NOT NULL DEFAULT 'oxiranker',
  source_domain_id BIGINT NOT NULL,
  source_article_id BIGINT NOT NULL,
  source_request_id BIGINT NULL,

  lang VARCHAR NOT NULL,
  lang_label VARCHAR NULL,
  dir VARCHAR NOT NULL,
  is_rtl BOOLEAN NULL,

  length VARCHAR NULL,
  topic_text TEXT NULL,

  slug VARCHAR NOT NULL,
  canonical_path VARCHAR NULL,
  effective_canonical_url TEXT NULL,

  title TEXT NOT NULL,
  meta_description TEXT NULL,
  html_body TEXT NULL,

  focus_keywords JSONB NOT NULL DEFAULT '[]'::jsonb,
  category VARCHAR NULL,
  tags JSONB NOT NULL DEFAULT '[]'::jsonb,

  reading_time_minutes INTEGER NULL,

  og_title TEXT NULL,
  og_description TEXT NULL,

  cover_image_url TEXT NULL,
  og_image_url TEXT NULL,

  article_schema JSONB NOT NULL DEFAULT '{}'::jsonb,

  raw_list_item JSONB NOT NULL DEFAULT '{}'::jsonb,
  raw_detail_payload JSONB NOT NULL DEFAULT '{}'::jsonb,

  source_created_at TIMESTAMPTZ NULL,
  source_updated_at TIMESTAMPTZ NULL,

  sync_status VARCHAR NOT NULL DEFAULT 'synced',
  last_synced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  last_sync_error TEXT NULL,

  publish_status VARCHAR NOT NULL DEFAULT 'draft',
  published_at TIMESTAMPTZ NULL,
  deleted_at TIMESTAMPTZ NULL,

  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

  source_cover_image_url TEXT NULL,
  source_og_image_url TEXT NULL,

  image_mirror_status VARCHAR NOT NULL DEFAULT 'pending',
  image_mirror_error TEXT NULL,
  image_mirrored_at TIMESTAMPTZ NULL,

  CONSTRAINT site_articles_dir_check
    CHECK (dir IN ('ltr', 'rtl')),

  CONSTRAINT site_articles_reading_time_check
    CHECK (reading_time_minutes IS NULL OR reading_time_minutes >= 0),

  CONSTRAINT site_articles_sync_status_check
    CHECK (sync_status IN ('pending', 'syncing', 'synced', 'failed')),

  CONSTRAINT site_articles_publish_status_check
    CHECK (publish_status IN ('draft', 'published', 'archived')),

  CONSTRAINT site_articles_image_mirror_status_check
    CHECK (image_mirror_status IN ('pending', 'not_needed', 'mirrored', 'failed')),

  CONSTRAINT site_articles_source_unique
    UNIQUE (source_provider, source_domain_id, source_article_id)
);
CREATE INDEX site_articles_source_lookup_idx
ON public.site_articles (source_provider, source_domain_id, source_article_id);

CREATE INDEX site_articles_source_updated_at_idx
ON public.site_articles (source_updated_at DESC);

CREATE INDEX site_articles_slug_idx
ON public.site_articles (slug)
WHERE deleted_at IS NULL;

CREATE UNIQUE INDEX site_articles_lang_slug_active_unique
ON public.site_articles (lang, slug)
WHERE deleted_at IS NULL;

CREATE INDEX site_articles_lang_publish_idx
ON public.site_articles (lang, publish_status)
WHERE deleted_at IS NULL;

CREATE INDEX site_articles_published_at_idx
ON public.site_articles (published_at DESC)
WHERE deleted_at IS NULL;

CREATE INDEX site_articles_focus_keywords_gin_idx
ON public.site_articles USING gin (focus_keywords);

CREATE INDEX site_articles_tags_gin_idx
ON public.site_articles USING gin (tags);

CREATE INDEX site_articles_article_schema_gin_idx
ON public.site_articles USING gin (article_schema);
Notă PostgreSQL
În PostgreSQL, pentru text persan sau Unicode se folosesc ghilimele simple. Ceva precum N'...' nu este necesar și nu trebuie folosit.
PASUL 22

Previne duplicatele cu UPSERT

Articolul nu trebuie inserat din nou pentru fiecare Webhook. Dacă există deja, trebuie actualizat.

Cheie principală pentru prevenirea duplicatelor
Cel mai important constraint pentru prevenirea duplicatelor este: UNIQUE (source_provider, source_domain_id, source_article_id)
INSERT INTO public.site_articles (
  source_provider,
  source_domain_id,
  source_article_id,
  source_request_id,
  lang,
  dir,
  slug,
  title,
  meta_description,
  html_body,
  focus_keywords,
  category,
  tags,
  reading_time_minutes,
  cover_image_url,
  og_image_url,
  article_schema,
  source_created_at,
  source_updated_at,
  sync_status,
  last_synced_at,
  publish_status,
  published_at,
  created_at,
  updated_at
)
VALUES (
  'oxiranker',
  1,
  5,
  5,
  'fa',
  'rtl',
  'smart-internal-links',
  'Article title',
  'Meta description',
  '<p>HTML body</p>',
  '[]'::jsonb,
  'SEO',
  '[]'::jsonb,
  7,
  'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
  'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
  '{}'::jsonb,
  '2026-05-20T19:33:23.618Z',
  '2026-05-20T19:34:09.629Z',
  'synced',
  now(),
  'published',
  now(),
  now(),
  now()
)
ON CONFLICT (source_provider, source_domain_id, source_article_id)
DO UPDATE SET
  source_request_id = EXCLUDED.source_request_id,
  lang = EXCLUDED.lang,
  dir = EXCLUDED.dir,
  slug = EXCLUDED.slug,
  title = EXCLUDED.title,
  meta_description = EXCLUDED.meta_description,
  html_body = EXCLUDED.html_body,
  focus_keywords = EXCLUDED.focus_keywords,
  category = EXCLUDED.category,
  tags = EXCLUDED.tags,
  reading_time_minutes = EXCLUDED.reading_time_minutes,
  cover_image_url = EXCLUDED.cover_image_url,
  og_image_url = EXCLUDED.og_image_url,
  article_schema = EXCLUDED.article_schema,
  source_created_at = EXCLUDED.source_created_at,
  source_updated_at = EXCLUDED.source_updated_at,
  sync_status = 'synced',
  last_synced_at = now(),
  last_sync_error = NULL,
  publish_status = CASE
    WHEN public.site_articles.publish_status = 'archived'
    THEN public.site_articles.publish_status
    ELSE 'published'
  END,
  published_at = COALESCE(public.site_articles.published_at, EXCLUDED.published_at, now()),
  deleted_at = NULL,
  updated_at = now();
PASUL 23

Fetch articol complet și sync după Webhook

Când sosește un Webhook, site-ul de destinație trebuie să facă fetch pentru articolul complet din Export API, să facă mirror imaginilor și să facă UPSERT articolului.

async function fetchOxirankerArticleDetail(args: {
  baseUrl: string;
  token: string;
  domainId: number;
  articleId: number;
}) {
  const url = `${args.baseUrl.replace(/\/+$/, "")}/api/v1/users/domains/${args.domainId}/articles/${args.articleId}/export/json`;

  const response = await fetch(url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${args.token}`,
      Accept: "application/json",
      "User-Agent": "My-Site-Oxiranker-Sync/1.0",
    },
  });

  const bodyText = await response.text();

  if (response.status < 200 || response.status > 299) {
    throw new Error(
      `Export API request failed with HTTP status ${response.status}. Response: ${bodyText.slice(0, 1000)}`
    );
  }

  try {
    return JSON.parse(bodyText);
  } catch {
    throw new Error("Export API returned invalid JSON.");
  }
}
async function syncArticleFromWebhook(payload: any) {
  const sourceDomainId = Number(payload.domain_id);
  const sourceArticleId = Number(payload.article_id);

  if (!Number.isFinite(sourceDomainId) || sourceDomainId <= 0) {
    throw new Error("Webhook domain id is invalid.");
  }

  if (!Number.isFinite(sourceArticleId) || sourceArticleId <= 0) {
    throw new Error("Webhook article id is invalid.");
  }

  const article = await fetchOxirankerArticleDetail({
    baseUrl: process.env.OXIRANKER_EXPORT_API_BASE_URL!,
    token: process.env.OXIRANKER_EXPORT_API_TOKEN!,
    domainId: sourceDomainId,
    articleId: sourceArticleId,
  });

  const mirroredImages = await mirrorArticleImages(article);

  const finalHtmlBody = replaceImageUrls(article.html_body, mirroredImages);
  const finalArticleSchema = replaceImageUrlsInJson(article.article_schema, mirroredImages);

  await upsertSiteArticle({
    source_provider: process.env.OXIRANKER_SOURCE_PROVIDER || "oxiranker",
    source_domain_id: sourceDomainId,
    source_article_id: sourceArticleId,
    source_request_id: article.request_id,
    lang: article.lang,
    dir: article.dir,
    slug: article.slug,
    title: article.title,
    meta_description: article.meta_description,
    html_body: finalHtmlBody,
    focus_keywords: article.focus_keywords,
    category: article.category,
    tags: article.tags,
    reading_time_minutes: article.reading_time_minutes,
    cover_image_url: mirroredImages.cover_image_url,
    og_image_url: mirroredImages.og_image_url,
    article_schema: finalArticleSchema,
    raw_detail_payload: article,
    source_created_at: article.created_at,
    source_updated_at: article.updated_at,
  });
}
PASUL 24

Image Mirroring și reguli de stocare a imaginilor

Site-ul de destinație trebuie să descarce imaginile, să le stocheze pe propria infrastructură și să înlocuiască URL-urile articolului.

Reguli Image Mirroring

Doar URL-urile HTTPS trebuie acceptate.
Fișierul trebuie să fie într-adevăr o imagine.
content-type trebuie să înceapă cu image/.
Fișierele goale nu trebuie stocate.
Dimensiunea fișierului nu trebuie să depășească limita.
Dacă cover și og sunt la fel, descarcă o singură dată.
După mirroring, înlocuiește URL-urile în html_body și article_schema.

Path de stocare recomandat

/uploads/site-articles/oxiranker/1/5/cover.webp
/uploads/site-articles/oxiranker/1/5/og.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
PASUL 25

Sync zilnic pentru fiabilitate

Webhook singur nu este suficient. Site-ul de destinație trebuie să aibă și un sync zilnic pentru recuperarea articolelor ratate.

De ce este necesar syncul zilnic?

Site-ul de destinație poate fi temporar indisponibil.
Webhook poate face timeout.
Pot apărea probleme de deployment sau de rețea.
Un articol poate fi generat, dar Webhookul lui poate fi ratat.

Algoritm sync zilnic

Pornește offset de la 0.
Folosește limit 20 sau 50.
Fă fetch pentru lista de articole din Export List API.
Pentru fiecare item, fă fetch pentru Export JSON al acelui articol.
Fă mirror imaginilor.
Stochează articolul cu UPSERT.
Crește offset și continuă până când total este complet.
PASUL 26

Afișează articolele pe site-ul de destinație

După stocarea articolelor în site_articles, site-ul de destinație nu trebuie să se conecteze la OxiRanker pentru fiecare afișare publică. Trebuie să citească din propria bază de date.

Pagină listă blog

SELECT
  id,
  lang,
  lang_label,
  dir,
  is_rtl,
  slug,
  title,
  meta_description,
  category,
  tags,
  reading_time_minutes,
  cover_image_url,
  og_image_url,
  source_created_at,
  published_at,
  created_at
FROM public.site_articles
WHERE source_provider = 'oxiranker'
  AND deleted_at IS NULL
  AND sync_status = 'synced'
  AND publish_status = 'published'
ORDER BY COALESCE(source_created_at, published_at, created_at) DESC, id DESC
LIMIT 20 OFFSET 0;

Pagină detaliu articol

SELECT *
FROM public.site_articles
WHERE source_provider = 'oxiranker'
  AND lang = 'fa'
  AND slug = 'smart-internal-links'
  AND deleted_at IS NULL
  AND sync_status = 'synced'
  AND publish_status = 'published'
LIMIT 1;
URL recomandat pentru articol
Pentru site-uri multilingve, poți folosi modelul /{lang}/blog/{slug}. Exemplu: /fa/blog/smart-internal-links
PASUL 27

SEO, Schema, RTL și HTML rendering

Site-ul de destinație trebuie să citească datele SEO din propria bază de date și să le renderizeze pe pagina articolului.

Câmpuri SEO

title pentru titlul paginii
meta_description pentru meta description
effective_canonical_url pentru canonical
og_title și og_description pentru Open Graph
og_image_url sau cover_image_url pentru imaginea de previzualizare socială
article_schema pentru JSON-LD

HTML rendering

<div
  className="article-content"
  dangerouslySetInnerHTML={{ __html: article.html_body }}
/>

Dacă site-ul de destinație are o politică strictă de securitate, poate sanitiza HTML înainte de salvare sau înainte de afișare.

RTL și LTR

Fiecare articol are câmpurile lang, dir și is_rtl. Dacă dir este rtl, pagina articolului trebuie renderizată de la dreapta la stânga.

<article dir="rtl">
  ...
</article>
PASUL 28

Erori comune și semnificațiile lor

Erorile site-ului de destinație trebuie să fie în engleză, clare și ușor de înțeles, astfel încât depanarea să se facă rapid.

Erori Export API

Tokenul de export al domeniului lipsește.
Tokenul Export API nu a fost trimis. Trebuie trimis Authorization: Bearer YOUR_EXPORT_TOKEN.
Tokenul de export al domeniului este invalid.
Tokenul este greșit, a fost rotit, nu are legătură cu acest domeniu sau domainId este greșit.
Tokenul nu are scope-ul necesar.
Tokenul nu are scope-ul necesar. Tokenurile noi trebuie să aibă articles:read și exports:read.
lang este invalid.
Valoarea lang din query parameter a fost trimisă incorect.

Erori Webhook

Webhook URL trebuie să folosească HTTPS.
Webhook URL trebuie să înceapă cu https://.
Webhook signature este invalidă.
Webhook Secret este greșit, secretul a fost rotit, raw body s-a schimbat sau signature a fost calculată incorect.
Webhook timestamp este expirat sau invalid.
Timestampul este vechi, în viitor sau invalid. Verifică server clock și NTP.
Livrarea testului Webhook a eșuat.
Site-ul de destinație nu a returnat un status de succes între 200 și 299, a făcut timeout sau nu a gestionat corect webhook.test.
PASUL 29

Checklist final pentru testarea API și Webhook

După implementare, aceste teste verifică conexiunea de la început până la final.

Test Export Token și JSON

GET https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=2&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Test Webhook

Dacă apelezi Webhookul de destinație cu Postman fără semnătură, trebuie să primești o eroare de securitate. Asta înseamnă că endpointul este activ și verifică securitatea.

{
  "error": "missing_webhook_signature",
  "message": "Webhook signature header is required."
}

Apoi apasă Test Webhook în OxiRanker. Dacă receiverul acceptă webhook.test, response_status trebuie să fie 200.

După rotire
Dacă Webhook Secret sau Export Token este rotit, copiază imediat noua valoare, înlocuiește-o în .env al site-ului de destinație și repornește sau redeployează backendul de destinație.
PASUL 30

Reguli importante pentru implementare corectă

Aceste reguli trebuie urmate în integrarea API pentru ca sistemul să rămână sigur, stabil și ușor de întreținut.

Export Token trebuie stocat doar în backend.
Webhook Secret trebuie stocat doar în backend.
Endpointul Webhook de destinație trebuie să fie exact /api/oxiranker/webhook.
Webhook URL trebuie să folosească HTTPS.
Webhook signature și timestamp trebuie verificate întotdeauna.
Articolele trebuie stocate cu UPSERT, nu cu un simplu insert.
Este necesar un unique constraint pentru prevenirea duplicatelor.
Imaginile trebuie descărcate din OxiRanker și stocate pe site-ul de destinație.
Site-ul de destinație nu trebuie să depindă permanent de URL-urile imaginilor OxiRanker.
URL-urile imaginilor trebuie înlocuite în html_body și article_schema.
Dacă Webhook eșuează, syncul zilnic trebuie să recupereze articolul ratat.
Erorile afișate utilizatorilor sau în loguri trebuie să fie în engleză și ușor de înțeles.
Dacă un articol este archived, syncul automat nu trebuie să îl publice din nou.
raw_detail_payload și raw_list_item trebuie păstrate, astfel încât niciun fel de date din outputul original să nu se piardă.
Dacă Export Token sau Webhook Secret este rotit, noua valoare trebuie înlocuită în .env, iar backendul trebuie repornit sau redeployat.
PASUL 31

Rezumat complet al fluxului API

După finalizarea acestor pași, site-ul tău personalizat poate primi, stoca, mirrora și publica în siguranță articole generate în OxiRanker.

Înregistrează-te în OxiRanker
Adaugă un domeniu
Verifică proprietatea domeniului
Alege limbile și adaugă Blog URL
Adaugă cuvinte-cheie pentru fiecare limbă
Completează Article Profile
Configurează Telegram dacă este necesar
Configurează motorul automat de articole
Creează primul articol din Generate
Verifică Preview și SEO Report
Obține Export Token din API Output
Obține endpointuri Export API gata
Obține Webhook Secret
Creează endpointul fix /api/oxiranker/webhook în backendul de destinație
Stochează Token și Secret în .env al site-ului de destinație
Creează tabelul site_articles și unique constraint
Implementează full article fetching din Export JSON
Implementează Image Mirroring
Implementează UPSERT
Implementează sync zilnic
Renderează articolul, SEO, OG și Schema din baza de date de destinație
Rulează testele finale Export API și Webhook