Documentation OxiRanker
Un centre de documentation clair pour connecter votre site web aux services OxiRanker. Commencez par l’intégration WordPress ou l’intégration API, puis suivez le guide étape par étape.
Service API
Utilisez Export API et Webhook pour les sites personnalisés, les plateformes CMS personnalisées, Next.js, Laravel, Node.js, PHP ou les backends Python.
OxiRanker n’est pas un hébergement permanent pour le site de destination
OxiRanker ne fournit aucune garantie d’hébergement permanent pour le contenu, les images ou les fichiers exportés du site de destination. OxiRanker n’est pas un service d’hébergement de contenu utilisateur, et le contenu généré, les images de couverture, les images Open Graph ou les fichiers de sortie peuvent ne pas rester sur l’infrastructure OxiRanker plus de 90 jours.
Par conséquent, après avoir reçu la sortie, le site de destination doit stocker l’article final, les images, les données SEO, les données Open Graph et les données Schema dans sa propre base de données, son propre serveur, son propre stockage ou CDN, et ne doit pas dépendre des URL OxiRanker pour l’affichage public.
Vidéo de formation à la connexion
Utilisez cette vidéo comme guide visuel avant de connecter WordPress ou d’implémenter l’intégration API.
Implémentez le flux de sortie étape par étape
Stockez d’abord l’Export Token et le Webhook Secret dans votre backend, puis complétez le Webhook receiver, la base de données, la logique UPSERT, l’Image Mirroring et la sync quotidienne.
S’inscrire et créer un domaine dans OxiRanker
Tout d’abord, inscrivez-vous sur OxiRanker, connectez-vous à votre compte et cliquez sur Add Domain depuis la section Domains.
Format correct du domaine
Dans le champ Domain name, saisissez uniquement le domaine racine. L’adresse ne doit pas inclure http, https ou www.
example.comFormats incorrects
N’entrez pas ces formats dans le champ du domaine.
https://example.com
http://example.com
www.example.comVérifier la propriété du domaine
Dans cette étape, vous devez prouver que le domaine vous appartient ou que vous avez accès à sa gestion.
DNS TXT
RecommandéAjoutez un TXT record au DNS du domaine. Pour Cloudflare, c’est généralement la méthode la plus simple.
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP File
Placez un fichier texte dans le chemin .well-known de votre site web afin qu’OxiRanker puisse le vérifier.
File URL:
https://example.com/.well-known/yourdomain-verification.txt
File Content:
220daaa37****42730f27Meta Tag
Placez une balise meta dans la section head de la page d’accueil de votre site web.
<meta name="yourdomain-verification" content="190208d****d275b65" />Recevoir des tokens gratuits après la vérification du premier domaine
Après la vérification du premier domaine, OxiRanker ajoute 1000 tokens gratuits à votre compte pour les tests initiaux.
Configurer les langues et le Blog URL
Dans la section Blog URLs, définissez si votre site web est monolingue ou multilingue et quelles langues doivent être utilisées pour les articles générés.
Choisir les langues
OxiRanker prend en charge 24 langues. Vous pouvez choisir une ou plusieurs langues selon la structure de votre site web.
Saisir le Blog URL
Pour chaque langue, saisissez le Blog URL correspondant à cette langue. Si votre site web est multilingue, saisissez chaque chemin de langue séparément.
https://example.com/fa/blog
https://example.com/en/blogAjouter des mots-clés pour chaque langue
Dans l’onglet Keywords, ajoutez au moins 3 et jusqu’à 50 mots-clés pour chaque langue.
Règle principale
Les mots-clés de chaque langue doivent être saisis dans la même langue. Si la langue de l’article est l’anglais, les mots-clés doivent également être en anglais.
Comment ajouter des mots-clés
Après avoir saisi chaque mot-clé, appuyez sur Enter pour l’ajouter. Après avoir complété tous les mots-clés, cliquez sur Save.
Compléter l’Article Profile du domaine
L’Article Profile indique à OxiRanker ce qu’est votre marque, pour qui elle écrit, quel ton elle doit utiliser et quels sujets elle doit éviter.
Secteur
Définit le marché ou la catégorie d’activité. Exemple : conseil immobilier et achat-vente de biens immobiliers à Dubaï.
Description de l’activité
Explique ce que vend l’entreprise, qui elle sert, comment elle fonctionne et ce qui la rend différente.
Audience
Définit à qui le contenu doit s’adresser. Chaque audience peut être enregistrée séparément.
Ton
Contrôle le style et la voix du contenu généré, comme formel, éducatif, simple, technique ou orienté vente.
Objectif du contenu
Définit ce que les articles doivent accomplir, comme attirer des clients, augmenter le trafic ou éduquer les utilisateurs.
Sujets interdits
Tout ce dont la marque ne doit pas parler ou à quoi elle ne doit pas être associée est saisi ici.
CTA optionnel
CTA signifie l’action que vous voulez que le lecteur effectue après avoir lu l’article. Si vous n’êtes pas sûr du texte approprié, il est préférable de laisser ce champ vide.
Liens internes et externes
OxiRanker peut appliquer intelligemment des liens internes et externes dans les articles.
Liens internes
Cette option est activée par défaut et il est préférable de la garder activée. Les liens internes aident les moteurs de recherche à mieux comprendre la structure de contenu de votre site web.
Dans le premier article, les liens internes ne sont généralement pas créés car il n’y a pas encore d’article précédent. À partir du deuxième article, le système peut effectuer le maillage interne.
Liens externes
Les liens externes sont ajoutés vers des sources officielles et fiables et sont marqués comme nofollow afin que l’autorité SEO ne soit pas transférée involontairement.
Vérifier la qualité du profil avec l’AI
Après avoir complété l’Article Profile, OxiRanker examine la qualité du profil.
Connecter Telegram pour les résumés d’articles
Après avoir enregistré le profil avec succès, vous pouvez activer l’envoi de résumés d’articles vers un canal Telegram.
Ajouter le bot
Ajoutez @Oxiranker_Bot à votre canal Telegram et faites-en un Admin.
Saisir le nom d’utilisateur du canal
Saisissez le nom du canal avec @. Exemple : @my_channel
Activer l’envoi du résumé
Après avoir enregistré un canal valide, activez l’envoi des résumés d’articles.
Configurer le moteur automatique d’articles
Dans l’onglet Automatic article generation, vous pouvez gérer le moteur de génération automatique d’articles.
Sélection intelligente du sujet
Le système utilise les mots-clés et l’Article Profile pour sélectionner des sujets adaptés, uniques et publiables.
Génération adaptée à la marque
Le ton, l’audience, l’objectif du contenu et les restrictions de la marque sont respectés pendant la génération de l’article.
Publication planifiée
La capacité mensuelle est répartie tout au long du mois afin que la publication de contenu paraisse plus naturelle et régulière.
Capacité mensuelle
7 articles / month
10 articles / month
15 articles / month
30 articles / month30 articles signifie presque un article par jour. 15 articles signifie presque un jour sur deux. 10 articles signifie environ tous les deux à trois jours. 7 articles signifie environ tous les trois à quatre jours.
Longueur de l’article et image
La longueur de l’article peut être Short, Standard ou Long. Standard est un choix équilibré pour la plupart des sites web.
Les images d’articles peuvent être activées ou désactivées, et le thème de l’image peut être Light ou Dark.
Créer le premier article depuis Generate
Après avoir préparé le domaine, ouvrez le menu Generate, choisissez le domaine et cliquez sur New Content ou New Article.
Langue
Une seule langue est sélectionnée pour chaque demande de contenu.
Sujet
Écrivez un sujet clair. Source URL est optionnel.
Image
Activez ou désactivez l’image et choisissez le thème Light ou Dark.
Paiement et génération
Le coût est déduit du wallet et la génération de l’article démarre.
Examiner Preview, SEO Report et JSON Output
Après la génération de l’article, plusieurs sorties importantes sont disponibles pour le contrôle qualité et la préparation de la publication.
Preview
Affiche le texte de l’article et l’image générée. Cette section est utile pour examiner la qualité finale avant publication.
SEO Report
Affiche un rapport SEO compréhensible incluant SEO Score, title, meta description, canonical, keywords, schema, headings, links, images et OpenGraph.
JSON Output
Affiche la sortie complète de l’article pour les utilisateurs techniques, incluant slug, html_body, schema, URL des images, tags, category, temps de lecture et timestamps de création/mise à jour.
Que font exactement Export API et Webhook ?
Après la génération d’un article dans OxiRanker, le site de destination peut le lire avec Export API ou être notifié via Webhook lorsque l’article est prêt.
Méthode 1 : Export API
Le site de destination peut lire les articles depuis OxiRanker avec l’Export Token chaque fois que nécessaire. Cette méthode est utilisée pour la sync manuelle, la sync quotidienne, les pages de liste d’articles et la récupération du JSON complet de l’article.
GET https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonGET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonMéthode 2 : Webhook
Lorsqu’un nouvel article est créé ou qu’un article est mis à jour, OxiRanker notifie l’endpoint du site de destination. Webhook n’envoie pas l’article complet. Il envoie seulement l’identifiant de l’article afin que le site de destination puisse récupérer l’article complet depuis l’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"
}Responsabilité du stockage de contenu et Image Mirroring
Le site de destination doit stocker l’article final et les images sur sa propre infrastructure.
Ce que le site de destination doit stocker
Exemple correct de chemin d’image
Utiliser directement et durablement les URL d’images OxiRanker n’est pas correct. L’image doit être téléchargée et les URL de l’article doivent être remplacées par les URL internes du site de destination.
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpObtenir l’Export Token et les endpoints prêts
L’Export Token et les endpoints prêts à l’emploi sont disponibles depuis la page de l’article, l’onglet Outputs et la section API Output.
Où obtenir l’Export Token
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}Endpoints prêts à l’emploi
Sur la même page d’article, dans la section API Output, Ready-to-use endpoints affiche les URL préparées.
https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=20&offset=0https://oxiranker.com/api/v1/users/domains/1/articles/1/export/jsonhttps://oxiranker.com/api/v1/users/domains/1/articles/1/export/html?canonical_mode=absolute&include_css=0Créer Webhook Secret et l’endpoint de destination
Webhook Secret est également créé depuis la page de l’article, l’onglet Outputs et la section API Output.
Où Webhook Secret est-il créé ?
Endpoint fixe de destination
OxiRanker accepte uniquement le chemin fixe suivant pour le Webhook de destination. Le site de destination doit créer cet endpoint dans son backend.
POST /api/oxiranker/webhook
https://example.com/api/oxiranker/webhookCet endpoint doit uniquement accepter POST, préserver le raw body, vérifier timestamp et signature, et retourner une réponse réussie pour webhook.test.
Valeurs .env requises pour le site de destination
OxiRanker n’a pas accès aux fichiers du site de destination ni à son .env. Le développeur du site de destination doit stocker ces valeurs dans son propre 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=50Valeurs de sécurité
Valeurs de stockage
Export API : List, JSON, HTML et ZIP
Le site de destination peut utiliser Export API pour recevoir la liste des articles ou la sortie complète de chaque article.
Obtenir la liste des articles pour un domaine
GET https://oxiranker.com/api/v1/users/domains/:domainId/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonlimit définit le nombre d’articles et est actuellement limité entre 1 et 50. offset est utilisé pour la pagination. lang est optionnel et sert à récupérer les articles pour une langue spécifique comme fa, en ou tr.
Obtenir l’article complet en JSON
GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonC’est la sortie la plus importante pour le site de destination. Le site de destination doit stocker le JSON complet de l’article dans sa propre base de données et lire depuis sa propre base de données pour l’affichage public.
Obtenir le HTML prêt
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/html?canonical_mode=absolute&include_css=0
Authorization: Bearer YOUR_EXPORT_TOKENHTML convient aux sites qui veulent une sortie prête, mais l’approche la plus professionnelle consiste à stocker JSON et à le rendre avec le propre template du site de destination.
Obtenir la sortie ZIP
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/zip
Authorization: Bearer YOUR_EXPORT_TOKENLa sortie ZIP inclut article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt et README.txt.
Signature Webhook et réception sécurisée du Webhook
Le site de destination doit vérifier la signature Webhook avec le raw body original.
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=...Formule de signature
payload_to_sign = timestamp + "." + rawBody
signature = "sha256=" + HMAC_SHA256(webhook_secret, payload_to_sign)Si le site de destination parse d’abord JSON puis le stringify à nouveau, l’ordre ou les espaces peuvent changer et la signature peut devenir invalide. Le raw body original doit être utilisé pour la vérification de la signature.
Exemple simple de code Node.js pour recevoir Webhook
Cet exemple Express gère raw body, timestamp, signature, webhook.test et les événements d’article.
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,
});
});Base de données recommandée pour le site de destination
Le site de destination doit stocker les articles dans sa propre base de données et empêcher les doublons avec une 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);Empêcher les doublons avec UPSERT
L’article ne doit pas être inséré à nouveau pour chaque Webhook. S’il existe déjà, il doit être mis à jour.
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();Récupérer l’article complet et synchroniser après Webhook
Lorsqu’un Webhook arrive, le site de destination doit récupérer l’article complet depuis Export API, mettre les images en miroir et faire un UPSERT de l’article.
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,
});
}Image Mirroring et règles de stockage des images
Le site de destination doit télécharger les images, les stocker sur sa propre infrastructure et remplacer les URL de l’article.
Règles d’Image Mirroring
Chemin de stockage recommandé
/uploads/site-articles/oxiranker/1/5/cover.webp
/uploads/site-articles/oxiranker/1/5/og.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpSync quotidienne pour la fiabilité
Webhook seul ne suffit pas. Le site de destination doit également avoir une sync quotidienne pour récupérer les articles manqués.
Pourquoi la sync quotidienne est-elle nécessaire ?
Algorithme de sync quotidienne
Afficher les articles sur le site de destination
Après avoir stocké les articles dans site_articles, le site de destination ne doit pas se connecter à OxiRanker pour chaque affichage public. Il doit lire depuis sa propre base de données.
Page de liste du 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;Page de détail de l’article
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;SEO, Schema, RTL et rendu HTML
Le site de destination doit lire les données SEO depuis sa propre base de données et les afficher sur la page de l’article.
Champs SEO
Rendu HTML
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.html_body }}
/>Si le site de destination a une politique de sécurité stricte, il peut nettoyer le HTML avant l’enregistrement ou avant l’affichage.
RTL et LTR
Chaque article possède les champs lang, dir et is_rtl. Si dir est rtl, la page de l’article doit être rendue de droite à gauche.
<article dir="rtl">
...
</article>Erreurs courantes et leurs significations
Les erreurs du site de destination doivent être en anglais, claires et compréhensibles afin que le dépannage puisse être effectué rapidement.
Erreurs Export API
Erreurs Webhook
Checklist finale de test API et Webhook
Après l’implémentation, ces tests vérifient la connexion de bout en bout.
Test Export Token et JSON
GET https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=2&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonGET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonTest Webhook
Si vous appelez le Webhook de destination avec Postman sans signature, vous devez recevoir une erreur de sécurité. Cela signifie que l’endpoint est actif et vérifie la sécurité.
{
"error": "missing_webhook_signature",
"message": "Webhook signature header is required."
}Cliquez ensuite sur Test Webhook dans OxiRanker. Si le receiver prend en charge webhook.test, response_status doit être 200.
Règles importantes pour une implémentation correcte
Ces règles doivent être suivies dans l’intégration API afin que le système reste sécurisé, stable et maintenable.
Résumé du flux API complet
Après avoir terminé ces étapes, votre site personnalisé peut recevoir, stocker, mettre en miroir et publier en toute sécurité les articles générés dans OxiRanker.