DOCUMENTAZIONE SVILUPPATORI OXIRANKER

Documentazione OxiRanker

Un centro di documentazione chiaro per collegare il tuo sito web ai servizi OxiRanker. Inizia con l’integrazione WordPress o con l’integrazione API, poi segui la guida passo dopo passo.

Documentazione ufficialeFull-feature V1Per proprietari di siti, sviluppatori e team di integrazione
Menu documentazione
Scegli una guida dal menu. Questa pagina è il centro principale della documentazione di OxiRanker.
Servizio API

Servizio API

Usa Export API e Webhook per siti personalizzati, piattaforme CMS personalizzate, Next.js, Laravel, Node.js, PHP o backend Python.

Dominio
Verificato
Contenuto
Pronto
API
Webhook
Avviso importante sull’archiviazione

OxiRanker non è un hosting permanente per il sito di destinazione

OxiRanker non fornisce una garanzia di hosting permanente per i contenuti, le immagini o i file esportati del sito di destinazione. OxiRanker non è un servizio di hosting per contenuti degli utenti e i contenuti generati, le immagini di copertina, le immagini Open Graph o i file di output potrebbero non rimanere sull’infrastruttura di OxiRanker per più di 90 giorni.

Pertanto, dopo aver ricevuto l’output, il sito di destinazione deve salvare l’articolo finale, le immagini, i dati SEO, i dati Open Graph e i dati Schema nel proprio database, server, storage o CDN, e non deve dipendere dagli URL di OxiRanker per la visualizzazione pubblica.

Video formativo

Video formativo sulla connessione

Usa questo video come guida visiva prima di collegare WordPress o implementare l’integrazione API.

Output API

Implementa il flusso di output passo dopo passo

Salva prima Export Token e Webhook Secret nel tuo backend, poi completa Webhook receiver, database, logica UPSERT, Image Mirroring e sync giornaliero.

PASSAGGIO 01

Registrarsi e creare un dominio in OxiRanker

Per prima cosa, registrati in OxiRanker, accedi al tuo account e clicca su Add Domain dalla sezione Domains.

Formato corretto del dominio

Nel campo Domain name, inserisci solo il dominio root. L’indirizzo non deve includere http, https o www.

example.com

Formati errati

Non inserire questi formati nel campo del dominio.

https://example.com
http://example.com
www.example.com
Dopo aver creato il dominio
Se il formato del dominio è valido, OxiRanker crea il dominio e ti porta al passaggio di verifica della proprietà del dominio.
PASSAGGIO 02

Verificare la proprietà del dominio

In questo passaggio devi dimostrare che il dominio appartiene a te o che hai accesso per gestirlo.

DNS TXT

Consigliato

Aggiungi un TXT record al DNS del dominio. Per Cloudflare, questo è di solito il metodo più semplice.

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

Inserisci un file di testo nel percorso .well-known del tuo sito web in modo che OxiRanker possa verificarlo.

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

File Content:
220daaa37****42730f27

Meta Tag

Inserisci un meta tag nella sezione head della homepage del tuo sito web.

<meta name="yourdomain-verification" content="190208d****d275b65" />
Nota DNS importante
Dopo aver aggiunto il DNS record, specialmente su Cloudflare, potresti dover attendere 2 o 3 minuti. Poi clicca su Confirm nella stessa pagina di OxiRanker.
Durata del token di verifica
I valori di verifica del dominio hanno un limite di tempo. Attualmente, i valori di verifica sono solitamente validi per 1440 minuti.
PASSAGGIO 03

Ricevere token gratuiti dopo la verifica del primo dominio

Dopo aver verificato il primo dominio, OxiRanker aggiunge 1000 token gratuiti al tuo account per i test iniziali.

Solo per il primo dominio
Questo regalo viene aggiunto solo per il primo dominio verificato e non viene aggiunto di nuovo per i domini successivi.
PASSAGGIO 04

Configurare lingue e Blog URL

Nella sezione Blog URLs, definisci se il tuo sito web è monolingua o multilingua e quali lingue devono essere usate per gli articoli generati.

Scegliere le lingue

OxiRanker supporta 24 lingue. Puoi scegliere una o più lingue in base alla struttura del tuo sito web.

Inserire Blog URL

Per ogni lingua, inserisci il blog URL di quella lingua. Se il tuo sito è multilingua, inserisci separatamente il percorso di ogni lingua.

https://example.com/fa/blog
https://example.com/en/blog
Struttura URL
Se il tuo sito ha una struttura diversa, inserisci il vero blog URL del tuo sito. Il punto importante è che l’URL finale deve essere il percorso in cui gli articoli devono essere pubblicati.
PASSAGGIO 05

Aggiungere keyword per ogni lingua

Nel tab Keywords, aggiungi almeno 3 e fino a 50 keyword per ogni lingua.

Regola principale

Le keyword di ogni lingua devono essere inserite nella stessa lingua. Se la lingua dell’articolo è inglese, anche le keyword devono essere in inglese.

Come aggiungere keyword

Dopo aver digitato ogni keyword, premi Enter per aggiungerla. Dopo aver completato tutte le keyword, clicca su Save.

Errore comune
Se inserisci keyword turche per la lingua inglese, potrebbero comparire frasi turche nel contenuto inglese. La lingua delle keyword deve corrispondere alla lingua dell’articolo.
PASSAGGIO 06

Completare il Domain Article Profile

Article Profile indica a OxiRanker cos’è il tuo brand, per chi scrive, quale tono deve usare e quali argomenti deve evitare.

Settore

Definisce il mercato o la categoria aziendale. Esempio: consulenza immobiliare e compravendita di immobili a Dubai.

Descrizione dell’attività

Spiega cosa vende l’azienda, chi serve, come funziona e cosa la rende diversa.

Pubblico

Definisce a chi deve rivolgersi il contenuto. Ogni pubblico può essere salvato separatamente.

Tono

Controlla lo stile e la voce del contenuto generato, come formale, educativo, semplice, tecnico o orientato alla vendita.

Obiettivo del contenuto

Definisce cosa devono ottenere gli articoli, come attirare clienti, aumentare il traffico o educare gli utenti.

Argomenti vietati

Qualsiasi cosa di cui il brand non dovrebbe parlare o a cui non dovrebbe essere associato viene inserita qui.

CTA opzionale

CTA significa l’azione che vuoi che il lettore compia dopo aver letto l’articolo. Se non sei sicuro di quale testo sia adatto, è meglio lasciare vuoto questo campo.

Profilo multilingua
Se il tuo sito è multilingua, devi completare il profilo solo in una lingua. OxiRanker può preparare le versioni richieste per le altre lingue in base allo stesso profilo.
PASSAGGIO 07

Link interni ed esterni

OxiRanker può applicare in modo intelligente link interni ed esterni all’interno degli articoli.

Link interni

Questa opzione è attiva di default ed è meglio mantenerla attiva. I link interni aiutano i motori di ricerca a comprendere meglio la struttura dei contenuti del tuo sito.

Nel primo articolo, di solito i link interni non vengono creati perché non esiste ancora un articolo precedente. Dal secondo articolo in poi, il sistema può eseguire il linking interno.

Link esterni

I link esterni vengono aggiunti a fonti ufficiali e affidabili e sono contrassegnati come nofollow, così l’autorità SEO non viene trasferita involontariamente.

PASSAGGIO 08

Controllare la qualità del profilo con AI

Dopo aver completato l’Article Profile, OxiRanker revisiona la qualità del profilo.

85+
Punteggio minimo richiesto per salvare
95
Punteggio eccellente spesso raggiunto dopo i miglioramenti
AI
Suggerimenti professionali per ogni campo
Se il punteggio è inferiore a 85
Il sistema non permette il salvataggio finale e mostra spiegazioni e suggerimenti di miglioramento per ogni campo problematico. Puoi applicare i suggerimenti e salvare di nuovo.
PASSAGGIO 09

Collegare Telegram per i riepiloghi degli articoli

Dopo aver salvato correttamente il profilo, puoi abilitare l’invio dei riepiloghi degli articoli a un canale Telegram.

1

Aggiungere il bot

Aggiungi @Oxiranker_Bot al tuo canale Telegram e rendilo Admin.

2

Inserire lo username del canale

Inserisci il nome del canale con @. Esempio: @my_channel

3

Abilitare l’invio del riepilogo

Dopo aver registrato un canale valido, abilita l’invio del riepilogo degli articoli.

Vantaggio di Telegram
Se abilitato, dopo la generazione del contenuto, il riepilogo dell’articolo viene inviato al canale Telegram con immagine e link. Questo può rafforzare i percorsi di traffico verso il tuo sito.
PASSAGGIO 10

Configurare il motore automatico degli articoli

Nel tab Automatic article generation, puoi gestire il motore di generazione automatica degli articoli.

Selezione intelligente dell’argomento

Il sistema usa keyword e Article Profile per selezionare argomenti adatti, unici e pubblicabili.

Generazione consapevole del brand

Tono, pubblico, obiettivo del contenuto e restrizioni del brand vengono rispettati durante la generazione dell’articolo.

Pubblicazione programmata

La capacità mensile viene distribuita durante il mese, così la pubblicazione dei contenuti appare più naturale e costante.

Capacità mensile

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

30 articoli significa quasi un articolo al giorno. 15 articoli significa quasi un giorno sì e uno no. 10 articoli significa circa ogni due o tre giorni. 7 articoli significa circa ogni tre o quattro giorni.

Lunghezza dell’articolo e immagine

La lunghezza dell’articolo può essere Short, Standard o Long. Standard è una scelta equilibrata per la maggior parte dei siti.

Le immagini degli articoli possono essere abilitate o disabilitate, e il tema dell’immagine può essere Light o Dark.

Nota per siti API
Il motore automatico genera gli articoli dentro OxiRanker. Per spostare ogni articolo nel tuo sito personalizzato dopo la generazione, devi implementare Export API, Webhook, archiviazione nel database e Image Mirroring.
PASSAGGIO 11

Creare il primo articolo da Generate

Dopo aver preparato il dominio, apri il menu Generate, scegli il dominio e clicca su New Content o New Article.

1

Lingua

Per ogni richiesta di contenuto viene selezionata una sola lingua.

2

Argomento

Scrivi un argomento chiaro. Source URL è opzionale.

3

Immagine

Abilita o disabilita l’immagine e scegli il tema Light o Dark.

4

Pagamento e generazione

Il costo viene detratto dal wallet e la generazione dell’articolo inizia.

Tempo di generazione
La generazione di articolo e immagine di solito richiede circa 2 o 3 minuti. Dopo il completamento, l’articolo è visibile dentro OxiRanker.
Source URL
Se viene inserito Source URL, OxiRanker lo usa per comprendere meglio l’argomento, ma non genera contenuto copiato. Il sistema è progettato per creare contenuto unico.
PASSAGGIO 12

Revisionare Preview, SEO Report e JSON Output

Dopo la generazione dell’articolo, sono disponibili diversi output importanti per la revisione della qualità e la preparazione alla pubblicazione.

Preview

Mostra il testo dell’articolo e l’immagine generata. Questa sezione è utile per revisionare la qualità finale prima della pubblicazione.

SEO Report

Mostra un report SEO comprensibile che include SEO Score, title, meta description, canonical, keywords, schema, headings, links, images e OpenGraph.

JSON Output

Mostra l’output completo dell’articolo per utenti tecnici, inclusi slug, html_body, schema, URL immagini, tags, category, tempo di lettura e timestamp di creazione/aggiornamento.

Significato degli output
Questi output mostrano che l’articolo non è solo testo semplice. È contenuto strutturato, pronto per la SEO e preparato per WordPress, CMS, API e sistemi di pubblicazione.
PASSAGGIO 13

Cosa fanno esattamente Export API e Webhook?

Dopo che un articolo viene generato in OxiRanker, il sito di destinazione può leggerlo con Export API o ricevere una notifica tramite Webhook quando l’articolo è pronto.

Metodo 1: Export API

Il sito di destinazione può leggere gli articoli da OxiRanker con Export Token quando necessario. Questo metodo viene usato per sync manuale, sync giornaliero, pagine elenco articoli e recupero del JSON completo dell’articolo.

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

Metodo 2: Webhook

Quando viene creato un nuovo articolo o un articolo viene aggiornato, OxiRanker notifica l’endpoint del sito di destinazione. Webhook non invia l’articolo completo. Invia solo l’identificatore dell’articolo, così il sito di destinazione può recuperare l’articolo completo dalla 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"
}
Nota molto importante
Webhook è solo un messaggio leggero. Il sito di destinazione non deve aspettarsi l’articolo completo dentro il Webhook. Dopo aver ricevuto domain_id e article_id, deve recuperare l’articolo completo dalla Export JSON API e salvarlo nel proprio database.
PASSAGGIO 14

Responsabilità di archiviazione dei contenuti e Image Mirroring

Il sito di destinazione deve salvare l’articolo finale e le immagini sulla propria infrastruttura.

Cosa deve salvare il sito di destinazione

Testo completo dell’articolo nel database del sito di destinazione
Immagine principale dell’articolo sul server, storage o CDN di destinazione
Immagine Open Graph sul server, storage o CDN di destinazione
Dati SEO, OpenGraph e Article Schema
raw_detail_payload e raw_list_item per preservare l’output originale

Esempio corretto di percorso immagine

Usare gli URL delle immagini OxiRanker direttamente e in modo permanente non è corretto. L’immagine deve essere scaricata e gli URL dell’articolo devono essere sostituiti con gli URL interni del sito di destinazione.

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker non è un hosting permanente per il sito di destinazione
OxiRanker non è responsabile dell’archiviazione permanente dei contenuti di output e delle immagini per il sito di destinazione. OxiRanker non è un servizio di hosting per contenuti degli utenti e i contenuti generati, le immagini di copertina, le immagini Open Graph o i file di output potrebbero non rimanere sull’infrastruttura di OxiRanker per più di 90 giorni. Pertanto, il sito di destinazione deve salvare l’articolo finale, le immagini, i dati SEO, OpenGraph e Schema nel proprio database, server, storage o CDN.
PASSAGGIO 15

Ottenere Export Token ed endpoint pronti

Export Token ed endpoint pronti all’uso sono disponibili dalla pagina dell’articolo, dal tab Outputs e dalla sezione API Output.

Dove ottenere Export Token

Crea il primo articolo in OxiRanker.
Apri la pagina di quell’articolo.
Apri il tab Outputs.
Apri la sezione API Output.
Crea un token da Token management.
Il token completo viene mostrato una sola volta.
Salva il token nel backend del sito di destinazione o nel .env.
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}

Endpoint pronti all’uso

Nella stessa pagina dell’articolo, dentro la sezione API Output, Ready-to-use endpoints mostra URL preparati.

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
Rotazione del Token
Se Export Token viene ruotato nel pannello OxiRanker, il token precedente scade. Il nuovo valore deve sostituire immediatamente il vecchio valore nel .env o nell’ambiente backend del sito di destinazione, e il backend di destinazione deve essere riavviato o ridistribuito.
PASSAGGIO 16

Creare Webhook Secret ed endpoint di destinazione

Webhook Secret viene creato anche dalla pagina dell’articolo, dal tab Outputs e dalla sezione API Output.

Dove viene creato Webhook Secret?

Apri la pagina dell’articolo.
Apri il tab Outputs.
Apri la sezione API Output.
Crea un secret nella sezione Webhook Secret.
Il secret completo viene mostrato una sola volta.
Salva il secret nel .env del sito di destinazione.

Endpoint fisso di destinazione

OxiRanker accetta solo il seguente percorso fisso per il Webhook di destinazione. Il sito di destinazione deve creare questo endpoint nel proprio backend.

POST /api/oxiranker/webhook

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

Questo endpoint deve accettare solo POST, preservare il raw body, verificare timestamp e signature, e restituire una risposta corretta per webhook.test.

Eventi attivi
Nella versione attuale, gli eventi sono fissi: article.created e article.updated. Per testare la connessione, viene inviato il valore evento webhook.test.
PASSAGGIO 17

Valori .env richiesti per il sito di destinazione

OxiRanker non ha accesso ai file del sito di destinazione o al suo .env. Lo sviluppatore del sito di destinazione deve salvare questi valori nel proprio 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 di sicurezza

OXIRANKER_EXPORT_API_TOKEN
Il token Export API deve essere salvato solo nel backend.
OXIRANKER_WEBHOOK_SECRET
Webhook Secret deve essere salvato solo nel backend.
OXIRANKER_WEBHOOK_MAX_SKEW_MS
La differenza massima di tempo consentita per Webhook. 300000 significa 5 minuti.

Valori di archiviazione

SITE_ARTICLE_IMAGE_UPLOAD_DIR
Il percorso fisico per salvare le immagini mirrorate sul server di destinazione.
SITE_ARTICLE_IMAGE_MAX_BYTES
La dimensione massima consentita dell’immagine per download e archiviazione.
PUBLIC_BASE_URL
L’URL pubblico del sito di destinazione usato per creare gli URL finali di immagini e articoli.
Sicurezza Frontend
Export Token e Webhook Secret non devono essere inseriti nel codice frontend, JavaScript visibile all’utente, localStorage o oggetto window.
PASSAGGIO 18

Export API: List, JSON, HTML e ZIP

Il sito di destinazione può usare Export API per ricevere l’elenco degli articoli o l’output completo di ogni articolo.

Ottenere l’elenco degli articoli per un dominio

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

limit definisce il numero di articoli ed è attualmente limitato tra 1 e 50. offset viene usato per la paginazione. lang è opzionale e viene usato per recuperare articoli di una lingua specifica come fa, en o tr.

Ottenere l’articolo completo come JSON

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

Questo è l’output più importante per il sito di destinazione. Il sito di destinazione deve salvare il JSON completo dell’articolo nel proprio database e leggere dal proprio database per la visualizzazione pubblica.

Ottenere HTML pronto

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 è adatto ai siti che vogliono un output pronto, ma l’approccio più professionale è salvare JSON e renderizzarlo con il template del sito di destinazione.

Ottenere output ZIP

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

L’output ZIP include article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt e README.txt.

PASSAGGIO 19

Firma Webhook e ricezione sicura del Webhook

Il sito di destinazione deve verificare la firma del Webhook usando il raw body originale.

Headers Webhook

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 della firma

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

Se il sito di destinazione esegue prima il parse del JSON e poi lo stringifica di nuovo, ordine o spaziatura possono cambiare e la firma può diventare non valida. Per verificare la firma deve essere usato il raw body originale.

Confronto timing-safe
Non confrontare le firme con expected === received. In Node.js è meglio usare crypto.timingSafeEqual.
PASSAGGIO 20

Semplice esempio di codice Node.js per ricevere Webhook

Questo esempio Express gestisce raw body, timestamp, signature, webhook.test ed eventi articolo.

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,
  });
});
PASSAGGIO 21

Database consigliato per il sito di destinazione

Il sito di destinazione deve salvare gli articoli nel proprio database e prevenire i duplicati con una 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);
Nota PostgreSQL
In PostgreSQL, per testo persiano o Unicode si usano apici semplici. Qualcosa come N'...' non è necessario e non deve essere usato.
PASSAGGIO 22

Prevenire duplicati con UPSERT

L’articolo non deve essere inserito di nuovo per ogni Webhook. Se esiste già, deve essere aggiornato.

Chiave principale per prevenire duplicati
La constraint più importante per prevenire duplicati è: 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();
PASSAGGIO 23

Recuperare l’articolo completo e sincronizzare dopo Webhook

Quando arriva un Webhook, il sito di destinazione deve recuperare l’articolo completo da Export API, mirrorare le immagini ed eseguire UPSERT dell’articolo.

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,
  });
}
PASSAGGIO 24

Image Mirroring e regole di archiviazione delle immagini

Il sito di destinazione deve scaricare le immagini, salvarle sulla propria infrastruttura e sostituire gli URL dell’articolo.

Regole di Image Mirroring

Devono essere accettati solo URL HTTPS.
Il file deve essere realmente un’immagine.
content-type deve iniziare con image/.
I file vuoti non devono essere salvati.
La dimensione del file non deve superare il limite.
Se cover e og sono uguali, scarica una sola volta.
Dopo il mirroring, sostituisci gli URL dentro html_body e article_schema.

Percorso di archiviazione consigliato

/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
PASSAGGIO 25

Sync giornaliero per affidabilità

Webhook da solo non è sufficiente. Il sito di destinazione dovrebbe avere anche un sync giornaliero per recuperare gli articoli mancati.

Perché serve il sync giornaliero?

Il sito di destinazione potrebbe essere temporaneamente non disponibile.
Webhook potrebbe andare in timeout.
Potrebbero verificarsi problemi di deploy o di rete.
Un articolo potrebbe essere generato ma il suo Webhook potrebbe andare perso.

Algoritmo del sync giornaliero

Inizia offset da 0.
Usa limit 20 o 50.
Recupera l’elenco articoli dalla Export List API.
Per ogni elemento, recupera l’Export JSON di quell’articolo.
Mirrora le immagini.
Salva l’articolo con UPSERT.
Aumenta offset e continua finché total non è completato.
PASSAGGIO 26

Mostrare gli articoli sul sito di destinazione

Dopo aver salvato gli articoli in site_articles, il sito di destinazione non deve collegarsi a OxiRanker per ogni visualizzazione pubblica. Deve leggere dal proprio database.

Pagina elenco 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;

Pagina dettaglio articolo

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 articolo consigliato
Per siti multilingua, puoi usare lo schema /{lang}/blog/{slug}. Esempio: /fa/blog/smart-internal-links
PASSAGGIO 27

SEO, Schema, RTL e rendering HTML

Il sito di destinazione deve leggere i dati SEO dal proprio database e renderizzarli nella pagina articolo.

Campi SEO

title per il titolo della pagina
meta_description per la meta description
effective_canonical_url per canonical
og_title e og_description per Open Graph
og_image_url o cover_image_url per l’immagine di anteprima social
article_schema per JSON-LD

Rendering HTML

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

Se il sito di destinazione ha una policy di sicurezza rigida, può sanificare l’HTML prima del salvataggio o prima della visualizzazione.

RTL e LTR

Ogni articolo ha i campi lang, dir e is_rtl. Se dir è rtl, la pagina articolo deve essere renderizzata da destra a sinistra.

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

Errori comuni e loro significati

Gli errori del sito di destinazione devono essere in inglese, chiari e comprensibili, così il troubleshooting può essere fatto rapidamente.

Errori Export API

Token di export del dominio mancante.
Il token Export API non è stato inviato. Deve essere inviato Authorization: Bearer YOUR_EXPORT_TOKEN.
Token di export del dominio non valido.
Il token è errato, è stato ruotato, non è collegato a questo dominio oppure domainId è errato.
Il token non ha lo scope richiesto.
Il token non ha lo scope richiesto. I nuovi token devono avere articles:read e exports:read.
lang non valido.
Il valore lang nel query parameter è stato inviato in modo errato.

Errori Webhook

L’URL Webhook deve usare HTTPS.
L’URL Webhook deve iniziare con https://.
Firma Webhook non valida.
Webhook Secret è errato, il secret è stato ruotato, il raw body è cambiato oppure la firma è stata calcolata in modo errato.
Il timestamp del Webhook è scaduto o non valido.
Il timestamp è vecchio, è nel futuro o non è valido. Controlla l’orologio del server e NTP.
Invio del test Webhook non riuscito.
Il sito di destinazione non ha restituito uno stato di successo da 200 a 299, è andato in timeout o non ha gestito correttamente webhook.test.
PASSAGGIO 29

Checklist finale di test API e Webhook

Dopo l’implementazione, questi test verificano la connessione dall’inizio alla fine.

Test Export Token e 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

Se chiami il Webhook di destinazione con Postman senza firma, devi ricevere un errore di sicurezza. Questo significa che l’endpoint è attivo e controlla la sicurezza.

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

Poi clicca su Test Webhook dentro OxiRanker. Se il receiver supporta webhook.test, response_status deve essere 200.

Dopo la rotazione
Se Webhook Secret o Export Token viene ruotato, copia subito il nuovo valore, sostituiscilo nel .env del sito di destinazione e riavvia o ridistribuisci il backend di destinazione.
PASSAGGIO 30

Regole importanti per una corretta implementazione

Queste regole devono essere seguite nell’integrazione API affinché il sistema resti sicuro, stabile e manutenibile.

Export Token deve essere salvato solo nel backend.
Webhook Secret deve essere salvato solo nel backend.
L’endpoint Webhook di destinazione deve essere esattamente /api/oxiranker/webhook.
L’URL Webhook deve usare HTTPS.
La signature e il timestamp del Webhook devono sempre essere verificati.
Gli articoli devono essere salvati con UPSERT, non con un semplice insert.
È richiesta una unique constraint per prevenire duplicati.
Le immagini devono essere scaricate da OxiRanker e salvate sul sito di destinazione.
Il sito di destinazione non deve dipendere permanentemente dagli URL delle immagini OxiRanker.
Gli URL delle immagini devono essere sostituiti dentro html_body e article_schema.
Se Webhook fallisce, il sync giornaliero deve recuperare l’articolo mancato.
Gli errori mostrati agli utenti o nei log devono essere in inglese e comprensibili.
Se un articolo è archiviato, il sync automatico non deve pubblicarlo di nuovo.
raw_detail_payload e raw_list_item devono essere preservati, così nessun dato dell’output originale viene perso.
Se Export Token o Webhook Secret viene ruotato, il nuovo valore deve essere sostituito in .env e il backend deve essere riavviato o ridistribuito.
PASSAGGIO 31

Riepilogo completo del flusso API

Dopo aver completato questi passaggi, il tuo sito personalizzato può ricevere, salvare, mirrorare e pubblicare in modo sicuro gli articoli generati in OxiRanker.

Registrarsi in OxiRanker
Aggiungere un dominio
Verificare la proprietà del dominio
Scegliere le lingue e aggiungere Blog URL
Aggiungere keyword per ogni lingua
Completare Article Profile
Configurare Telegram se necessario
Configurare il motore automatico degli articoli
Creare il primo articolo da Generate
Revisionare Preview e SEO Report
Ottenere Export Token da API Output
Ottenere endpoint Export API pronti
Ottenere Webhook Secret
Creare l’endpoint fisso /api/oxiranker/webhook nel backend di destinazione
Salvare Token e Secret nel .env del sito di destinazione
Creare la tabella site_articles e unique constraint
Implementare il recupero dell’articolo completo da Export JSON
Implementare Image Mirroring
Implementare UPSERT
Implementare sync giornaliero
Renderizzare articolo, SEO, OG e Schema dal database di destinazione
Eseguire i test finali Export API e Webhook