Documentación de OxiRanker
Un centro de documentación claro para conectar tu sitio web con los servicios de OxiRanker. Empieza con la integración de WordPress o la integración API y luego sigue la guía paso a paso.
Servicio API
Usa Export API y Webhook para sitios web personalizados, plataformas CMS personalizadas, Next.js, Laravel, Node.js, PHP o backends de Python.
OxiRanker no es hosting permanente para el sitio web de destino
OxiRanker no ofrece una garantía de hosting permanente para el contenido, las imágenes o los archivos exportados del sitio web de destino. OxiRanker no es un servicio de hosting de contenido de usuarios, y el contenido generado, las imágenes de portada, las imágenes Open Graph o los archivos de salida pueden no permanecer en la infraestructura de OxiRanker durante más de 90 días.
Por lo tanto, después de recibir la salida, el sitio web de destino debe almacenar el artículo final, las imágenes, los datos SEO, los datos Open Graph y los datos Schema en su propia base de datos, servidor, almacenamiento o CDN, y no debe depender de las URL de OxiRanker para la visualización pública.
Video de formación de conexión
Usa este video como recorrido visual antes de conectar WordPress o implementar la integración API.
Implementa el flujo de salida paso a paso
Guarda primero el Export Token y el Webhook Secret en tu backend, luego completa el Webhook receiver, la base de datos, la lógica UPSERT, Image Mirroring y el sync diario.
Registrarse y crear un dominio en OxiRanker
Primero, regístrate en OxiRanker, inicia sesión en tu cuenta y haz clic en Add Domain desde la sección Domains.
Formato correcto del dominio
En el campo Domain name, introduce solo el dominio raíz. La dirección no debe incluir http, https ni www.
example.comFormatos incorrectos
No introduzcas estos formatos en el campo del dominio.
https://example.com
http://example.com
www.example.comVerificar la propiedad del dominio
En este paso, debes demostrar que el dominio te pertenece o que tienes acceso para administrarlo.
DNS TXT
RecomendadoAñade un TXT record al DNS del dominio. Para Cloudflare, este suele ser el método más simple.
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP File
Coloca un archivo de texto en la ruta .well-known de tu sitio web para que OxiRanker pueda verificarlo.
File URL:
https://example.com/.well-known/yourdomain-verification.txt
File Content:
220daaa37****42730f27Meta Tag
Coloca un meta tag dentro de la sección head de la página de inicio de tu sitio web.
<meta name="yourdomain-verification" content="190208d****d275b65" />Recibir tokens gratuitos después de verificar el primer dominio
Después de verificar el primer dominio, OxiRanker añade 1000 tokens gratuitos a tu cuenta para pruebas iniciales.
Configurar idiomas y Blog URL
En la sección Blog URLs, define si tu sitio web es de un solo idioma o multilingüe y qué idiomas deben usarse para los artículos generados.
Elegir idiomas
OxiRanker admite 24 idiomas. Puedes elegir uno o varios idiomas según la estructura de tu sitio web.
Introducir Blog URL
Para cada idioma, introduce la URL del blog de ese idioma. Si tu sitio web es multilingüe, introduce cada ruta de idioma por separado.
https://example.com/fa/blog
https://example.com/en/blogAñadir palabras clave para cada idioma
En la pestaña Keywords, añade al menos 3 y hasta 50 palabras clave para cada idioma.
Regla principal
Las palabras clave de cada idioma deben introducirse en el mismo idioma. Si el idioma del artículo es inglés, las palabras clave también deben estar en inglés.
Cómo añadir palabras clave
Después de escribir cada palabra clave, pulsa Enter para añadirla. Después de completar todas las palabras clave, haz clic en Save.
Completar el Article Profile del dominio
Article Profile indica a OxiRanker qué es tu marca, para quién escribe, qué tono debe usar y qué temas debe evitar.
Industria
Define el mercado o la categoría del negocio. Ejemplo: consultoría inmobiliaria y compra y venta de propiedades en Dubái.
Descripción del negocio
Explica qué vende el negocio, a quién sirve, cómo funciona y qué lo hace diferente.
Audiencia
Define a quién debe dirigirse el contenido. Cada audiencia puede guardarse por separado.
Tono
Controla el estilo y la voz del contenido generado, como formal, educativo, simple, técnico u orientado a ventas.
Objetivo del contenido
Define qué deben lograr los artículos, como atraer clientes, aumentar el tráfico o educar a los usuarios.
Temas prohibidos
Cualquier cosa de la que la marca no deba hablar o con la que no deba asociarse se introduce aquí.
CTA opcional
CTA significa la acción que quieres que el lector realice después de leer el artículo. Si no estás seguro de qué texto es adecuado, es mejor dejar este campo vacío.
Enlaces internos y externos
OxiRanker puede aplicar inteligentemente enlaces internos y externos dentro de los artículos.
Enlaces internos
Esta opción está activada por defecto y es mejor mantenerla activada. Los enlaces internos ayudan a los motores de búsqueda a entender mejor la estructura de contenido de tu sitio web.
En el primer artículo, normalmente no se crean enlaces internos porque todavía no hay ningún artículo anterior. A partir del segundo artículo, el sistema puede realizar enlaces internos.
Enlaces externos
Los enlaces externos se añaden a fuentes oficiales y confiables y se marcan como nofollow para que la autoridad SEO no se transfiera involuntariamente.
Comprobar la calidad del perfil con AI
Después de completar el Article Profile, OxiRanker revisa la calidad del perfil.
Conectar Telegram para resúmenes de artículos
Después de guardar correctamente el perfil, puedes activar el envío de resúmenes de artículos a un canal de Telegram.
Añadir el bot
Añade @Oxiranker_Bot a tu canal de Telegram y hazlo Admin.
Introducir el nombre de usuario del canal
Introduce el nombre del canal con @. Ejemplo: @my_channel
Activar envío de resumen
Después de registrar un canal válido, activa el envío de resúmenes de artículos.
Configurar el motor automático de artículos
En la pestaña Automatic article generation, puedes gestionar el motor automático de generación de artículos.
Selección inteligente de temas
El sistema usa palabras clave y Article Profile para seleccionar temas adecuados, únicos y publicables.
Generación consciente de la marca
Durante la generación del artículo se respetan el tono, la audiencia, el objetivo del contenido y las restricciones de la marca.
Publicación programada
La capacidad mensual se distribuye a lo largo del mes para que la publicación de contenido parezca más natural y consistente.
Capacidad mensual
7 articles / month
10 articles / month
15 articles / month
30 articles / month30 artículos significa casi un artículo al día. 15 artículos significa casi día por medio. 10 artículos significa aproximadamente cada dos o tres días. 7 artículos significa aproximadamente cada tres o cuatro días.
Longitud del artículo e imagen
La longitud del artículo puede ser Short, Standard o Long. Standard es una opción equilibrada para la mayoría de los sitios web.
Las imágenes de artículos pueden activarse o desactivarse, y el tema de la imagen puede ser Light o Dark.
Crear el primer artículo desde Generate
Después de preparar el dominio, abre el menú Generate, elige el dominio y haz clic en New Content o New Article.
Idioma
Solo se selecciona un idioma para cada solicitud de contenido.
Tema
Escribe un tema claro. Source URL es opcional.
Imagen
Activa o desactiva la imagen y elige el tema Light o Dark.
Pago y generación
El coste se descuenta del wallet y comienza la generación del artículo.
Revisar Preview, SEO Report y JSON Output
Después de generar el artículo, hay varias salidas importantes disponibles para revisar la calidad y preparar la publicación.
Preview
Muestra el texto del artículo y la imagen generada. Esta sección es útil para revisar la calidad final antes de publicar.
SEO Report
Muestra un informe SEO comprensible que incluye SEO Score, title, meta description, canonical, keywords, schema, headings, links, images y OpenGraph.
JSON Output
Muestra la salida completa del artículo para usuarios técnicos, incluyendo slug, html_body, schema, URLs de imágenes, tags, category, tiempo de lectura y timestamps de creación/actualización.
¿Qué hacen exactamente Export API y Webhook?
Después de que se genera un artículo en OxiRanker, el sitio web de destino puede leerlo con Export API o recibir una notificación mediante Webhook cuando el artículo esté listo.
Método 1: Export API
El sitio web de destino puede leer artículos desde OxiRanker con el Export Token cuando sea necesario. Este método se usa para sync manual, sync diario, páginas de lista de artículos y obtención del JSON completo del artículo.
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étodo 2: Webhook
Cuando se crea un nuevo artículo o se actualiza un artículo, OxiRanker notifica al endpoint del sitio web de destino. Webhook no envía el artículo completo. Solo envía el identificador del artículo para que el sitio web de destino pueda obtener el artículo completo desde la 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"
}Responsabilidad de almacenamiento de contenido e Image Mirroring
El sitio web de destino debe almacenar el artículo final y las imágenes en su propia infraestructura.
Qué debe almacenar el sitio web de destino
Ejemplo correcto de ruta de imagen
Usar las URL de imágenes de OxiRanker directamente y de forma permanente no es correcto. La imagen debe descargarse y las URL del artículo deben reemplazarse por URL internas del sitio web de destino.
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpObtener Export Token y endpoints listos
Export Token y los endpoints listos para usar están disponibles desde la página del artículo, la pestaña Outputs y la sección API Output.
Dónde obtener el Export Token
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}Endpoints listos para usar
En la misma página del artículo, dentro de la sección API Output, Ready-to-use endpoints muestra URLs preparadas.
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=0Crear Webhook Secret y endpoint de destino
Webhook Secret también se crea desde la página del artículo, la pestaña Outputs y la sección API Output.
¿Dónde se crea Webhook Secret?
Endpoint fijo de destino
OxiRanker solo acepta la siguiente ruta fija para el Webhook de destino. El sitio web de destino debe crear este endpoint en su backend.
POST /api/oxiranker/webhook
https://example.com/api/oxiranker/webhookEste endpoint solo debe aceptar POST, preservar el raw body, verificar timestamp y signature, y devolver una respuesta exitosa para webhook.test.
Valores .env requeridos para el sitio web de destino
OxiRanker no tiene acceso a los archivos del sitio web de destino ni a su .env. El desarrollador del sitio web de destino debe almacenar estos valores en su propio 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=50Valores de seguridad
Valores de almacenamiento
Export API: List, JSON, HTML y ZIP
El sitio web de destino puede usar Export API para recibir la lista de artículos o la salida completa de cada artículo.
Obtener la lista de artículos para un dominio
GET https://oxiranker.com/api/v1/users/domains/:domainId/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonlimit define el número de artículos y actualmente está limitado entre 1 y 50. offset se usa para paginación. lang es opcional y se usa para obtener artículos de un idioma específico como fa, en o tr.
Obtener el artículo completo como JSON
GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonEsta es la salida más importante para el sitio web de destino. El sitio web de destino debe almacenar el JSON completo del artículo en su propia base de datos y leer desde su propia base de datos para la visualización pública.
Obtener HTML listo
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/html?canonical_mode=absolute&include_css=0
Authorization: Bearer YOUR_EXPORT_TOKENHTML es adecuado para sitios web que quieren una salida lista, pero el enfoque más profesional es almacenar JSON y renderizarlo con la propia plantilla del sitio web de destino.
Obtener salida ZIP
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/zip
Authorization: Bearer YOUR_EXPORT_TOKENLa salida ZIP incluye article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt y README.txt.
Firma de Webhook y recepción segura de Webhook
El sitio web de destino debe verificar la firma de Webhook usando el raw body original.
Headers de 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=...Fórmula de firma
payload_to_sign = timestamp + "." + rawBody
signature = "sha256=" + HMAC_SHA256(webhook_secret, payload_to_sign)Si el sitio web de destino parsea JSON primero y luego lo vuelve a convertir en string, el orden o los espacios pueden cambiar y la firma puede volverse inválida. El raw body original debe usarse para la verificación de firma.
Ejemplo simple de código Node.js para recibir Webhook
Este ejemplo de Express maneja raw body, timestamp, signature, webhook.test y eventos de artículos.
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 datos recomendada para el sitio web de destino
El sitio web de destino debe almacenar artículos en su propia base de datos y prevenir duplicados 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);Prevenir duplicados con UPSERT
El artículo no debe insertarse de nuevo por cada Webhook. Si ya existe, debe actualizarse.
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();Obtener el artículo completo y sincronizar después del Webhook
Cuando llega un Webhook, el sitio web de destino debe obtener el artículo completo desde Export API, reflejar las imágenes y hacer UPSERT del artículo.
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 y reglas de almacenamiento de imágenes
El sitio web de destino debe descargar imágenes, almacenarlas en su propia infraestructura y reemplazar las URL del artículo.
Reglas de Image Mirroring
Ruta de almacenamiento recomendada
/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 diario para fiabilidad
Webhook por sí solo no es suficiente. El sitio web de destino también debería tener un sync diario para recuperar artículos perdidos.
¿Por qué se necesita sync diario?
Algoritmo de sync diario
Mostrar artículos en el sitio web de destino
Después de almacenar artículos en site_articles, el sitio web de destino no debe conectarse a OxiRanker para cada visualización pública. Debe leer desde su propia base de datos.
Página de lista del 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;Página de detalle del artículo
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 y renderizado HTML
El sitio web de destino debe leer los datos SEO desde su propia base de datos y renderizarlos en la página del artículo.
Campos SEO
Renderizado HTML
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.html_body }}
/>Si el sitio web de destino tiene una política de seguridad estricta, puede sanitizar HTML antes de guardarlo o antes de mostrarlo.
RTL y LTR
Cada artículo tiene campos lang, dir e is_rtl. Si dir es rtl, la página del artículo debe renderizarse de derecha a izquierda.
<article dir="rtl">
...
</article>Errores comunes y sus significados
Los errores del sitio web de destino deben estar en inglés, ser claros y comprensibles para que la resolución de problemas se pueda hacer rápidamente.
Errores de Export API
Errores de Webhook
Checklist final de pruebas de API y Webhook
Después de la implementación, estas pruebas verifican la conexión de principio a fin.
Prueba de Export Token y 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/jsonPrueba de Webhook
Si llamas al Webhook de destino con Postman sin una firma, debes recibir un error de seguridad. Esto significa que el endpoint está activo y verifica la seguridad.
{
"error": "missing_webhook_signature",
"message": "Webhook signature header is required."
}Luego haz clic en Test Webhook dentro de OxiRanker. Si el receiver admite webhook.test, response_status debe ser 200.
Reglas importantes para una implementación correcta
Estas reglas deben seguirse en la integración API para que el sistema permanezca seguro, estable y mantenible.
Resumen completo del flujo API
Después de completar estos pasos, tu sitio web personalizado puede recibir, almacenar, reflejar y publicar de forma segura artículos generados en OxiRanker.