OxiRanker-Dokumentation
Ein übersichtliches Dokumentationszentrum zur Verbindung deiner Website mit OxiRanker-Diensten. Beginne mit der WordPress-Integration oder der API-Integration und folge anschließend der Anleitung Schritt für Schritt.
API-Dienst
Nutze Export API und Webhook für individuelle Websites, eigene CMS-Plattformen, Next.js-, Laravel-, Node.js-, PHP- oder Python-Backends.
OxiRanker ist kein dauerhaftes Hosting für die Zielwebsite
OxiRanker bietet keine Garantie für dauerhaftes Hosting von Inhalten, Bildern oder exportierten Dateien der Zielwebsite. OxiRanker ist kein Hosting-Dienst für Benutzerinhalte, und generierte Inhalte, Titelbilder, Open-Graph-Bilder oder Ausgabedateien bleiben möglicherweise nicht länger als 90 Tage auf der OxiRanker-Infrastruktur.
Deshalb muss die Zielwebsite nach dem Empfang der Ausgabe den finalen Artikel, Bilder, SEO-Daten, Open-Graph-Daten und Schema-Daten in ihrer eigenen Datenbank, auf ihrem eigenen Server, in ihrem eigenen Speicher oder CDN speichern und darf sich für die öffentliche Anzeige nicht auf OxiRanker-URLs verlassen.
Schulungsvideo zur Verbindung
Nutze dieses Video als visuelle Schritt-für-Schritt-Anleitung, bevor du WordPress verbindest oder die API-Integration implementierst.
Implementiere den Ausgabeablauf Schritt für Schritt
Speichere zuerst den Export Token und das Webhook Secret in deinem Backend und vervollständige anschließend Webhook Receiver, Datenbank, UPSERT-Logik, Image Mirroring und tägliche Synchronisierung.
Registrieren und eine Domain in OxiRanker erstellen
Registriere dich zuerst bei OxiRanker, melde dich in deinem Konto an und klicke im Bereich Domains auf Add Domain.
Richtiges Domainformat
Gib im Feld Domain name nur die Root-Domain ein. Die Adresse darf weder http noch https noch www enthalten.
example.comFalsche Formate
Gib diese Formate nicht in das Domainfeld ein.
https://example.com
http://example.com
www.example.comDomaininhaberschaft verifizieren
In diesem Schritt musst du nachweisen, dass die Domain dir gehört oder dass du Zugriff auf ihre Verwaltung hast.
DNS TXT
EmpfohlenFüge einen TXT-Record zur Domain-DNS hinzu. Bei Cloudflare ist dies normalerweise die einfachste Methode.
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP File
Lege eine Textdatei im .well-known-Pfad deiner Website ab, damit OxiRanker sie verifizieren kann.
File URL:
https://example.com/.well-known/yourdomain-verification.txt
File Content:
220daaa37****42730f27Meta Tag
Füge ein Meta-Tag in den head-Bereich der Startseite deiner Website ein.
<meta name="yourdomain-verification" content="190208d****d275b65" />Kostenlose Token nach Verifizierung der ersten Domain erhalten
Nach der Verifizierung der ersten Domain fügt OxiRanker deinem Konto 1000 kostenlose Token für erste Tests hinzu.
Sprachen und Blog-URL konfigurieren
Lege im Bereich Blog URLs fest, ob deine Website einsprachig oder mehrsprachig ist und welche Sprachen für generierte Artikel verwendet werden sollen.
Sprachen auswählen
OxiRanker unterstützt 24 Sprachen. Du kannst je nach Struktur deiner Website eine oder mehrere Sprachen auswählen.
Blog-URL eingeben
Gib für jede Sprache die Blog-URL dieser Sprache ein. Wenn deine Website mehrsprachig ist, gib jeden Sprachpfad separat ein.
https://example.com/fa/blog
https://example.com/en/blogKeywords für jede Sprache hinzufügen
Füge im Tab Keywords für jede Sprache mindestens 3 und höchstens 50 Keywords hinzu.
Hauptregel
Keywords für jede Sprache müssen in derselben Sprache eingegeben werden. Wenn die Artikelsprache Englisch ist, müssen auch die Keywords Englisch sein.
So fügst du Keywords hinzu
Drücke nach dem Eingeben jedes Keywords Enter, um es hinzuzufügen. Wenn alle Keywords vollständig sind, klicke auf Save.
Das Domain Article Profile vervollständigen
Das Article Profile teilt OxiRanker mit, was deine Marke ist, für wen geschrieben wird, welchen Ton es verwenden soll und welche Themen vermieden werden sollen.
Branche
Definiert den Markt oder die Geschäftskategorie. Beispiel: Immobilienberatung sowie Kauf und Verkauf von Immobilien in Dubai.
Geschäftsbeschreibung
Erklärt, was das Unternehmen verkauft, wen es bedient, wie es funktioniert und was es besonders macht.
Zielgruppe
Definiert, wen die Inhalte ansprechen sollen. Jede Zielgruppe kann separat gespeichert werden.
Ton
Steuert Stil und Stimme der generierten Inhalte, zum Beispiel formell, lehrreich, einfach, technisch oder verkaufsorientiert.
Inhaltsziel
Definiert, was die Artikel erreichen sollen, zum Beispiel Kunden gewinnen, Traffic steigern oder Nutzer informieren.
Verbotene Themen
Alles, worüber die Marke nicht sprechen oder womit sie nicht in Verbindung gebracht werden soll, wird hier eingetragen.
Optionaler CTA
CTA bedeutet die Handlung, die der Leser nach dem Lesen des Artikels ausführen soll. Wenn du nicht sicher bist, welcher Text geeignet ist, ist es besser, dieses Feld leer zu lassen.
Interne und externe Verlinkung
OxiRanker kann interne und externe Links intelligent innerhalb von Artikeln anwenden.
Interne Links
Diese Option ist standardmäßig aktiviert und sollte am besten aktiviert bleiben. Interne Verlinkung hilft Suchmaschinen, die Inhaltsstruktur deiner Website besser zu verstehen.
Im ersten Artikel werden interne Links normalerweise nicht erstellt, weil noch kein vorheriger Artikel vorhanden ist. Ab dem zweiten Artikel kann das System interne Verlinkung durchführen.
Externe Links
Externe Links werden zu offiziellen und vertrauenswürdigen Quellen hinzugefügt und als nofollow markiert, damit SEO-Autorität nicht unbeabsichtigt übertragen wird.
Profilqualität mit AI prüfen
Nach dem Ausfüllen des Article Profile überprüft OxiRanker die Profilqualität.
Telegram für Artikelzusammenfassungen verbinden
Nach erfolgreichem Speichern des Profils kannst du den Versand von Artikelzusammenfassungen an einen Telegram-Kanal aktivieren.
Bot hinzufügen
Füge @Oxiranker_Bot zu deinem Telegram-Kanal hinzu und mache ihn zum Admin.
Kanal-Benutzernamen eingeben
Gib den Kanalnamen mit @ ein. Beispiel: @my_channel
Zusammenfassungsversand aktivieren
Aktiviere nach der Registrierung eines gültigen Kanals den Versand von Artikelzusammenfassungen.
Die automatische Artikel-Engine konfigurieren
Im Tab Automatic article generation kannst du die automatische Artikel-Engine verwalten.
Intelligente Themenauswahl
Das System nutzt Keywords und Article Profile, um passende, einzigartige und veröffentlichungsfähige Themen auszuwählen.
Markenbewusste Generierung
Ton, Zielgruppe, Inhaltsziel und Markenbeschränkungen werden während der Artikelerstellung berücksichtigt.
Geplante Veröffentlichung
Die monatliche Kapazität wird über den Monat verteilt, damit die Veröffentlichung von Inhalten natürlicher und konsistenter wirkt.
Monatliche Kapazität
7 articles / month
10 articles / month
15 articles / month
30 articles / month30 Artikel bedeuten fast einen Artikel pro Tag. 15 Artikel bedeuten fast jeden zweiten Tag. 10 Artikel bedeuten etwa alle zwei bis drei Tage. 7 Artikel bedeuten etwa alle drei bis vier Tage.
Artikellänge und Bild
Die Artikellänge kann Short, Standard oder Long sein. Standard ist für die meisten Websites eine ausgewogene Wahl.
Artikelbilder können aktiviert oder deaktiviert werden, und das Bilddesign kann Light oder Dark sein.
Den ersten Artikel über Generate erstellen
Nachdem die Domain vorbereitet ist, öffne das Menü Generate, wähle die Domain aus und klicke auf New Content oder New Article.
Sprache
Für jede Inhaltsanfrage wird nur eine Sprache ausgewählt.
Thema
Schreibe ein klares Thema. Source URL ist optional.
Bild
Aktiviere oder deaktiviere das Bild und wähle das Design Light oder Dark.
Zahlung und Generierung
Die Kosten werden vom Wallet abgezogen und die Artikelerstellung startet.
Preview, SEO Report und JSON Output prüfen
Nach der Generierung des Artikels stehen mehrere wichtige Ausgaben zur Qualitätsprüfung und Vorbereitung der Veröffentlichung zur Verfügung.
Preview
Zeigt den Artikeltext und das generierte Bild. Dieser Bereich ist hilfreich, um die finale Qualität vor der Veröffentlichung zu prüfen.
SEO Report
Zeigt einen verständlichen SEO-Bericht mit SEO Score, title, meta description, canonical, Keywords, schema, Überschriften, Links, Bildern und OpenGraph.
JSON Output
Zeigt die vollständige Artikelausgabe für technische Nutzer, einschließlich slug, html_body, schema, Bild-URLs, Tags, Kategorie, Lesezeit und Erstellungs-/Aktualisierungszeitpunkten.
Was genau machen Export API und Webhook?
Nachdem ein Artikel in OxiRanker generiert wurde, kann die Zielwebsite ihn über Export API lesen oder per Webhook benachrichtigt werden, wenn der Artikel bereit ist.
Methode 1: Export API
Die Zielwebsite kann Artikel bei Bedarf mit dem Export Token aus OxiRanker lesen. Diese Methode wird für manuelle Synchronisierung, tägliche Synchronisierung, Artikellistenseiten und das Abrufen des vollständigen Artikel-JSON verwendet.
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/jsonMethode 2: Webhook
Wenn ein neuer Artikel erstellt oder ein Artikel aktualisiert wird, benachrichtigt OxiRanker den Endpunkt der Zielwebsite. Webhook sendet nicht den vollständigen Artikel. Er sendet nur die Artikelkennung, damit die Zielwebsite den vollständigen Artikel aus der Export JSON API abrufen kann.
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"
}Verantwortung für Inhaltsspeicherung und Image Mirroring
Die Zielwebsite muss den finalen Artikel und die Bilder auf ihrer eigenen Infrastruktur speichern.
Was die Zielwebsite speichern muss
Beispiel für einen richtigen Bildpfad
Die direkte und dauerhafte Verwendung von OxiRanker-Bild-URLs ist nicht korrekt. Das Bild muss heruntergeladen werden und Artikel-URLs müssen durch interne URLs der Zielwebsite ersetzt werden.
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpExport Token und einsatzbereite Endpunkte erhalten
Export Token und einsatzbereite Endpunkte sind auf der Artikelseite, im Outputs-Tab und im Bereich API Output verfügbar.
Wo du den Export Token erhältst
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}Einsatzbereite Endpunkte
Auf derselben Artikelseite zeigt Ready-to-use endpoints im Bereich API Output vorbereitete URLs an.
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=0Webhook Secret und Ziel-Endpunkt erstellen
Webhook Secret wird ebenfalls auf der Artikelseite, im Outputs-Tab und im Bereich API Output erstellt.
Wo wird Webhook Secret erstellt?
Fester Ziel-Endpunkt
OxiRanker akzeptiert für den Ziel-Webhook nur den folgenden festen Pfad. Die Zielwebsite muss diesen Endpunkt in ihrem Backend erstellen.
POST /api/oxiranker/webhook
https://example.com/api/oxiranker/webhookDieser Endpunkt darf nur POST akzeptieren, muss den raw body beibehalten, timestamp und signature verifizieren und für webhook.test eine erfolgreiche Antwort zurückgeben.
Erforderliche .env-Werte für die Zielwebsite
OxiRanker hat keinen Zugriff auf die Dateien oder die .env der Zielwebsite. Der Entwickler der Zielwebsite muss diese Werte im eigenen Backend speichern.
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=50Sicherheitswerte
Speicherwerte
Export API: List, JSON, HTML und ZIP
Die Zielwebsite kann Export API verwenden, um die Artikelliste oder die vollständige Ausgabe jedes Artikels zu erhalten.
Artikelliste für eine Domain abrufen
GET https://oxiranker.com/api/v1/users/domains/:domainId/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonlimit definiert die Anzahl der Artikel und ist derzeit auf 1 bis 50 begrenzt. offset wird für Pagination verwendet. lang ist optional und wird verwendet, um Artikel für eine bestimmte Sprache wie fa, en oder tr abzurufen.
Den vollständigen Artikel als JSON abrufen
GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonDies ist die wichtigste Ausgabe für die Zielwebsite. Die Zielwebsite muss das vollständige Artikel-JSON in ihrer eigenen Datenbank speichern und für die öffentliche Anzeige aus ihrer eigenen Datenbank lesen.
Fertiges HTML abrufen
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/html?canonical_mode=absolute&include_css=0
Authorization: Bearer YOUR_EXPORT_TOKENHTML eignet sich für Websites, die eine fertige Ausgabe möchten. Der professionellere Ansatz ist jedoch, JSON zu speichern und es mit dem eigenen Template der Zielwebsite zu rendern.
ZIP-Ausgabe abrufen
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/zip
Authorization: Bearer YOUR_EXPORT_TOKENDie ZIP-Ausgabe enthält article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt und README.txt.
Webhook-Signatur und sicherer Webhook-Empfang
Die Zielwebsite muss die Webhook-Signatur mit dem ursprünglichen raw body verifizieren.
Webhook-Header
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=...Signaturformel
payload_to_sign = timestamp + "." + rawBody
signature = "sha256=" + HMAC_SHA256(webhook_secret, payload_to_sign)Wenn die Zielwebsite JSON zuerst parst und anschließend erneut stringifiziert, können sich Reihenfolge oder Abstände ändern und die Signatur ungültig werden. Für die Signaturverifizierung muss der ursprüngliche raw body verwendet werden.
Einfaches Node.js-Codebeispiel zum Empfangen von Webhook
Dieses Express-Beispiel verarbeitet raw body, timestamp, signature, webhook.test und Artikel-Events.
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,
});
});Empfohlene Datenbank für die Zielwebsite
Die Zielwebsite muss Artikel in ihrer eigenen Datenbank speichern und Duplikate mit einer eindeutigen Einschränkung verhindern.
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);Duplikate mit UPSERT verhindern
Der Artikel darf nicht bei jedem Webhook erneut eingefügt werden. Wenn er bereits existiert, muss er aktualisiert werden.
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();Den vollständigen Artikel nach dem Webhook abrufen und synchronisieren
Wenn ein Webhook eintrifft, muss die Zielwebsite den vollständigen Artikel aus Export API abrufen, die Bilder spiegeln und den Artikel per UPSERT speichern.
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 und Regeln zur Bildspeicherung
Die Zielwebsite muss Bilder herunterladen, auf ihrer eigenen Infrastruktur speichern und Artikel-URLs ersetzen.
Regeln für Image Mirroring
Empfohlener Speicherpfad
/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.webpTägliche Synchronisierung für Zuverlässigkeit
Webhook allein reicht nicht aus. Die Zielwebsite sollte zusätzlich eine tägliche Synchronisierung haben, um verpasste Artikel wiederherzustellen.
Warum ist tägliche Synchronisierung nötig?
Algorithmus der täglichen Synchronisierung
Artikel auf der Zielwebsite anzeigen
Nachdem Artikel in site_articles gespeichert wurden, darf die Zielwebsite nicht bei jeder öffentlichen Anzeige eine Verbindung zu OxiRanker herstellen. Sie muss aus ihrer eigenen Datenbank lesen.
Blog-Listenseite
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;Artikeldetailseite
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 und HTML-Rendering
Die Zielwebsite muss SEO-Daten aus ihrer eigenen Datenbank lesen und auf der Artikelseite rendern.
SEO-Felder
HTML-Rendering
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.html_body }}
/>Wenn die Zielwebsite eine strikte Sicherheitsrichtlinie hat, kann sie HTML vor dem Speichern oder vor der Anzeige bereinigen.
RTL und LTR
Jeder Artikel hat die Felder lang, dir und is_rtl. Wenn dir rtl ist, muss die Artikelseite von rechts nach links gerendert werden.
<article dir="rtl">
...
</article>Häufige Fehler und ihre Bedeutung
Fehler der Zielwebsite müssen auf Englisch, klar und verständlich sein, damit Fehlerbehebung schnell durchgeführt werden kann.
Export-API-Fehler
Webhook-Fehler
Abschließende API- und Webhook-Test-Checkliste
Nach der Implementierung prüfen diese Tests die Verbindung von Anfang bis Ende.
Export-Token- und JSON-Test
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/jsonWebhook-Test
Wenn du den Ziel-Webhook mit Postman ohne Signatur aufrufst, musst du einen Sicherheitsfehler erhalten. Das bedeutet, dass der Endpunkt aktiv ist und Sicherheit prüft.
{
"error": "missing_webhook_signature",
"message": "Webhook signature header is required."
}Klicke anschließend in OxiRanker auf Test Webhook. Wenn der Empfänger webhook.test unterstützt, muss response_status 200 sein.
Wichtige Regeln für die korrekte Implementierung
Diese Regeln müssen bei der API-Integration befolgt werden, damit das System sicher, stabil und wartbar bleibt.
Zusammenfassung des vollständigen API-Ablaufs
Nach Abschluss dieser Schritte kann deine individuelle Website von OxiRanker generierte Artikel sicher empfangen, speichern, spiegeln und veröffentlichen.