OXIRANKER DEVELOPER DOCS

OxiRanker-dokumentation

Ett tydligt dokumentationscenter för att ansluta din webbplats till OxiRanker-tjänster. Börja med WordPress-integration eller API-integration och följ sedan guiden steg för steg.

Officiell dokumentationFull-feature V1För webbplatsägare, utvecklare och integrationsteam
Dokumentationsmeny
Välj en guide från menyn. Den här sidan är OxiRankers huvudsakliga dokumentationscenter.
API-tjänst

API-tjänst

Använd Export API och Webhook för anpassade webbplatser, custom CMS-plattformar, Next.js, Laravel, Node.js, PHP eller Python backend.

Domain
Verifierad
Innehåll
Redo
API
Webhook
Viktigt meddelande om lagring

OxiRanker är inte permanent hosting för destination website

OxiRanker ger ingen garanti för permanent hosting av innehåll, bilder eller exported files för destination website. OxiRanker är inte en user-content hosting service, och generated content, cover images, Open Graph images eller output files kanske inte finns kvar på OxiRankers infrastruktur i mer än 90 dagar.

Därför måste destination website, efter att ha tagit emot output, lagra final article, images, SEO data, Open Graph data och Schema data i sin egen database, server, storage eller CDN, och får inte vara beroende av OxiRanker URLs för publik visning.

Utbildningsvideo

Utbildningsvideo för anslutning

Använd den här videon som en visuell genomgång innan du ansluter WordPress eller implementerar API-integrationen.

API Output

Implementera output flow steg för steg

Spara först Export Token och Webhook Secret i din backend, slutför sedan Webhook receiver, database, UPSERT logic, Image Mirroring och daily sync.

STEG 01

Registrera dig och skapa domain i OxiRanker

Registrera dig först i OxiRanker, sign in på ditt account och klicka på Add Domain i sektionen Domains.

Korrekt domain-format

I fältet Domain name ska du endast ange root domain. Adressen får inte innehålla http, https eller www.

example.com

Felaktiga format

Ange inte dessa format i domain-fältet.

https://example.com
http://example.com
www.example.com
Efter att domain har skapats
Om domain-formatet är valid skapar OxiRanker domain och tar dig till steget för domain ownership verification.
STEG 02

Verifiera domain-ägarskap

I det här steget måste du bevisa att domain tillhör dig eller att du har access för att hantera den.

DNS TXT

Rekommenderas

Lägg till en TXT record i domain DNS. För Cloudflare är detta vanligtvis den enklaste metoden.

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

Placera en text file i webbplatsens .well-known path så att OxiRanker kan verifiera den.

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

File Content:
220daaa37****42730f27

Meta Tag

Placera en meta tag i head section på webbplatsens homepage.

<meta name="yourdomain-verification" content="190208d****d275b65" />
Viktig DNS-notering
Efter att du har lagt till DNS record, särskilt i Cloudflare, kan du behöva vänta 2 till 3 minuter. Klicka sedan på Confirm på samma OxiRanker page.
Giltighetstid för verification token
Domain verification values har en time limit. För närvarande är verification values vanligtvis valid i 1440 minutes.
STEG 03

Ta emot gratis token efter verifiering av första domain

Efter att första domain har verifierats lägger OxiRanker till 1000 free tokens på ditt account för initial testing.

Endast för första domain
Den här gåvan läggs endast till för den första verified domain och läggs inte till igen för senare domains.
STEG 04

Konfigurera språk och Blog URL

I sektionen Blog URLs definierar du om din webbplats är single-language eller multilingual och vilka languages som ska användas för generated articles.

Välj språk

OxiRanker stöder 24 languages. Du kan välja ett eller flera languages baserat på webbplatsens struktur.

Ange Blog URL

För varje language anger du blog URL för det språket. Om din webbplats är multilingual anger du varje language path separat.

https://example.com/fa/blog
https://example.com/en/blog
URL-struktur
Om din webbplats har en annan struktur anger du den faktiska blog URL för din webbplats. Det viktiga är att final URL är den path där artiklarna ska publiceras.
STEG 05

Lägg till keywords för varje språk

I tabben Keywords lägger du till minst 3 och högst 50 keywords för varje språk.

Huvudregel

Keywords för varje språk måste anges på samma språk. Om article language är English måste keywords också vara English.

Så lägger du till keywords

Efter att du har skrivit varje keyword trycker du på Enter för att lägga till den. När alla keywords är klara klickar du på Save.

Vanligt misstag
Om du anger Turkish keywords för English language kan Turkish phrases visas i English content. Keyword language måste matcha article language.
STEG 06

Slutför domain Article Profile

Article Profile berättar för OxiRanker vad ditt brand är, vem det skriver för, vilken tone det ska använda och vilka topics det ska undvika.

Industry

Definierar market eller business category. Exempel: real estate consulting och property buying and selling i Dubai.

Business description

Förklarar vad business säljer, vem det betjänar, hur det fungerar och vad som gör det annorlunda.

Audience

Definierar vem content ska tala till. Varje audience kan sparas separat.

Tone

Styr style och voice för generated content, till exempel formal, educational, simple, technical eller sales-oriented.

Content goal

Definierar vad articles ska uppnå, till exempel att locka customers, öka traffic eller utbilda users.

Förbjudna ämnen

Allt som brand inte bör tala om eller inte bör associeras med anges här.

Optional CTA

CTA betyder den action du vill att reader ska göra efter att ha läst article. Om du inte är säker på vilken text som är suitable är det bättre att lämna detta field empty.

Multilingual profile
Om din webbplats är multilingual behöver du bara slutföra profile på ett language. OxiRanker kan förbereda nödvändiga versions för andra languages baserat på samma profile.
STEG 07

Intern och extern länkning

OxiRanker kan intelligent tillämpa internal links och external links i artiklar.

Internal links

Det här option är enabled som default och det är bäst att hålla det enabled. Internal linking hjälper search engines att förstå webbplatsens content structure bättre.

I den första article skapas internal links vanligtvis inte eftersom det ännu inte finns någon previous article. Från den andra article och framåt kan system utföra internal linking.

External links

External links läggs till official och trusted sources och markeras som nofollow så att SEO authority inte överförs unintentionally.

STEG 08

Kontrollera profile quality med AI

Efter att Article Profile är slutförd granskar OxiRanker profile quality.

85+
Minimum score som krävs för save
95
Excellent score som vanligtvis uppnås efter improvements
AI
Professional suggestions för varje field
Om score är under 85
System tillåter inte final saving och visar explanations och improvement suggestions för varje problematic field. Du kan tillämpa suggestions och save igen.
STEG 09

Anslut Telegram för article summaries

Efter att profile har saveats framgångsrikt kan du enable att article summaries skickas till en Telegram channel.

1

Lägg till bot

Lägg till @Oxiranker_Bot i din Telegram channel och gör den till Admin.

2

Ange channel username

Ange channel name med @. Exempel: @my_channel

3

Enable summary delivery

Efter att en valid channel har registrerats, enable article summary delivery.

Telegram-fördel
Om enabled skickas article summary efter content generation till Telegram channel med image och link. Detta kan stärka traffic paths till din webbplats.
STEG 10

Konfigurera automatic article engine

I tabben Automatic article generation kan du hantera automatic article generation engine.

Smart topic selection

System använder keywords och Article Profile för att välja suitable, unique och publishable topics.

Brand-aware generation

Tone, audience, content goal och brand restrictions respekteras under article generation.

Scheduled publishing

Monthly capacity distribueras över månaden så att content publishing ser mer natural och consistent ut.

Monthly capacity

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

30 articles betyder nästan en article per dag. 15 articles betyder nästan varannan dag. 10 articles betyder ungefär varannan till var tredje dag. 7 articles betyder ungefär var tredje till var fjärde dag.

Article length och image

Article length kan vara Short, Standard eller Long. Standard är ett balanced choice för de flesta webbplatser.

Article images kan vara enabled eller disabled, och image theme kan vara Light eller Dark.

Notering för API websites
Automatic engine genererar articles inuti OxiRanker. För att flytta varje article till din custom website efter generation måste du implement Export API, Webhook, database storage och Image Mirroring.
STEG 11

Skapa första article från Generate

Efter att domain har förberetts öppnar du Generate menu, väljer domain och klickar på New Content eller New Article.

1

Language

Endast ett language väljs för varje content request.

2

Topic

Skriv ett tydligt topic. Source URL är optional.

3

Image

Enable eller disable image och välj theme Light eller Dark.

4

Payment och generation

Cost dras från wallet och article generation startar.

Generation time
Article och image generation tar vanligtvis cirka 2 till 3 minuter. Efter slutförandet är article visible i OxiRanker.
Source URL
Om Source URL anges använder OxiRanker den för att bättre förstå topic, men genererar inte copied content. System är designed för att skapa unique content.
STEG 12

Granska Preview, SEO Report och JSON Output

Efter att article har generated finns flera important outputs tillgängliga för quality review och publishing preparation.

Preview

Visar article text och generated image. Den här section är användbar för att granska final quality före publishing.

SEO Report

Visar en understandable SEO report inklusive SEO Score, title, meta description, canonical, keywords, schema, headings, links, images och OpenGraph.

JSON Output

Visar full article output för technical users, inklusive slug, html_body, schema, image URLs, tags, category, reading time och created/updated timestamps.

Betydelsen av outputs
Dessa outputs visar att article inte bara är plain text. Det är structured, SEO-ready content förberett för WordPress, CMS, API och publishing systems.
STEG 13

Vad gör Export API och Webhook exakt?

Efter att en article har generated i OxiRanker kan destination website read den med Export API eller bli notified via Webhook när article är ready.

Metod 1: Export API

Destination website kan read articles från OxiRanker med Export Token när det behövs. Den här method används för manual sync, daily sync, article list pages och fetching full article JSON.

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

Metod 2: Webhook

När en new article created eller en article updated notify OxiRanker destination website endpoint. Webhook skickar inte full article. Den skickar bara article identifier så att destination website kan fetch full article från 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"
}
Mycket viktig note
Webhook är bara ett lightweight message. Destination website ska inte expect full article inuti Webhook. Efter att domain_id och article_id har tagits emot måste den fetch complete article från Export JSON API och store den i sin egen database.
STEG 14

Content storage responsibility och Image Mirroring

Destination website måste store final article och images på sin egen infrastructure.

Vad destination website måste store

Full article text i destination website database
Main article image på destination server, storage eller CDN
Open Graph image på destination server, storage eller CDN
SEO, OpenGraph och Article Schema data
raw_detail_payload och raw_list_item för att preserve original output

Korrekt exempel på image path

Att använda OxiRanker image URLs directly och permanently är inte korrekt. Image måste downloadas och article URLs måste replace med interna URLs för destination website.

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker är inte permanent hosting för destination website
OxiRanker ansvarar inte för permanently storing output content och images för destination website. OxiRanker är inte en user-content hosting service, och generated content, cover images, Open Graph images eller output files kanske inte finns kvar i OxiRanker infrastructure i mer än 90 days. Därför måste destination website store final article, images, SEO data, OpenGraph och Schema i sin egen database, server, storage eller CDN.
STEG 15

Hämta Export Token och ready endpoints

Export Token och ready-to-use endpoints finns tillgängliga från article page, Outputs tab och API Output section.

Var du hämtar Export Token

Create första article i OxiRanker.
Open den article page.
Open Outputs tab.
Open API Output section.
Create token från Token management.
Full token shown endast en gång.
Store token i destination website backend eller .env.
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}

Ready-to-use endpoints

På samma article page, i API Output section, visar Ready-to-use endpoints prepared URLs.

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
Rotating Token
Om Export Token rotated i OxiRanker panel expires previous token. New value måste immediately replace old value i destination website .env eller backend environment, och destination backend måste restarted eller redeployed.
STEG 16

Skapa Webhook Secret och destination endpoint

Webhook Secret skapas också från article page, Outputs tab och API Output section.

Var skapas Webhook Secret?

Open article page.
Open Outputs tab.
Open API Output section.
Create secret i Webhook Secret section.
Full secret shown endast en gång.
Store secret i destination website .env.

Fixed destination endpoint

OxiRanker accepterar endast följande fixed path för destination Webhook. Destination website måste create denna endpoint i sin backend.

POST /api/oxiranker/webhook

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

Denna endpoint måste endast accept POST, preserve raw body, verify timestamp och signature, samt return successful response för webhook.test.

Active events
I current version är events fixed: article.created och article.updated. För testing connection skickas event value webhook.test.
STEG 17

Required .env values för destination website

OxiRanker har inte access till destination website files eller .env. Destination website developer måste store dessa values i sin egen 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

Security values

OXIRANKER_EXPORT_API_TOKEN
Export API token måste endast stored i backend.
OXIRANKER_WEBHOOK_SECRET
Webhook Secret måste endast stored i backend.
OXIRANKER_WEBHOOK_MAX_SKEW_MS
Maximum allowed time difference för Webhook. 300000 betyder 5 minutes.

Storage values

SITE_ARTICLE_IMAGE_UPLOAD_DIR
Physical path för storing mirrored images på destination server.
SITE_ARTICLE_IMAGE_MAX_BYTES
Maximum allowed image size för download och storage.
PUBLIC_BASE_URL
Public destination website URL som används för att create final image och article URLs.
Frontend security
Export Token och Webhook Secret får inte placeras i frontend code, user-visible JavaScript, localStorage eller window object.
STEG 18

Export API: List, JSON, HTML och ZIP

Destination website kan använda Export API för att receive article list eller full output för varje article.

Hämta list of articles för domain

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

limit definierar number of articles och är för närvarande limited mellan 1 och 50. offset används för pagination. lang är optional och används för att fetch articles för specific language som fa, en eller tr.

Hämta full article som JSON

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

Detta är den mest important output för destination website. Destination website måste store full article JSON i sin egen database och read från sin egen database för public display.

Hämta ready HTML

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 passar websites som vill ha ready output, men en mer professional approach är att store JSON och render det med destination website egen template.

Hämta ZIP output

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

ZIP output innehåller article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt och README.txt.

STEG 19

Webhook Signature och secure Webhook receiving

Destination website måste verify Webhook signature med original raw body.

Webhook headers

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

Signature formula

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

Om destination website först parse JSON och sedan stringify den igen kan order eller spacing ändras och signature kan bli invalid. Original raw body måste användas för signature verification.

Timing-safe comparison
Jämför inte signatures med expected === received. I Node.js är det bättre att använda crypto.timingSafeEqual.
STEG 20

Simple Node.js code sample för receiving Webhook

Detta Express sample handle raw body, timestamp, signature, webhook.test och article 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,
  });
});
STEG 21

Recommended destination website database

Destination website måste store articles i sin egen database och prevent duplicates med 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);
PostgreSQL note
I PostgreSQL används simple quotes för Persian eller Unicode text. Något som N'...' behövs inte och ska inte användas.
STEG 22

Förhindra duplicates med UPSERT

Article ska inte inserted igen för varje Webhook. Om den redan exists måste den updated.

Main key för preventing duplicates
Den viktigaste constraint för preventing duplicates är: 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();
STEG 23

Fetch full article och sync efter Webhook

När Webhook kommer måste destination website fetch full article från Export API, mirror images och UPSERT 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,
  });
}
STEG 24

Image Mirroring och image storage rules

Destination website måste download images, store dem på sin egen infrastructure och replace article URLs.

Image Mirroring rules

Endast HTTPS URLs måste accepted.
File måste verkligen vara image.
content-type måste börja med image/.
Empty files får inte stored.
File size får inte överskrida limit.
Om cover och og är samma, download endast en gång.
Efter mirroring, replace URLs i html_body och article_schema.

Recommended storage path

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

Daily sync för reliability

Webhook alone räcker inte. Destination website bör också ha daily sync för att recover missed articles.

Varför behövs daily sync?

Destination website kan vara temporarily down.
Webhook kan timeout.
Deployment eller network issues kan inträffa.
Article kan vara generated men dess Webhook kan missed.

Daily sync algorithm

Start offset från 0.
Använd limit 20 eller 50.
Fetch article list från Export List API.
För varje item, fetch Export JSON för den article.
Mirror images.
Store article med UPSERT.
Increase offset och continue tills total är completed.
STEG 26

Visa articles på destination website

Efter storing articles i site_articles ska destination website inte connect till OxiRanker för varje public display. Den måste read från sin egen database.

Blog list page

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;

Article detail page

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;
Recommended article URL
För multilingual websites kan du använda pattern /{lang}/blog/{slug}. Exempel: /fa/blog/smart-internal-links
STEG 27

SEO, Schema, RTL och HTML rendering

Destination website måste read SEO data från sin egen database och render dem på article page.

SEO fields

title för page title
meta_description för meta description
effective_canonical_url för canonical
og_title och og_description för Open Graph
og_image_url eller cover_image_url för social preview image
article_schema för JSON-LD

HTML rendering

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

Om destination website har strict security policy kan den sanitize HTML före saving eller före displaying.

RTL och LTR

Varje article har fields lang, dir och is_rtl. Om dir är rtl måste article page rendered right-to-left.

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

Common errors och deras betydelse

Destination website errors måste vara English, clear och understandable så att troubleshooting kan göras snabbt.

Export API errors

Domain export token saknas.
Export API token skickades inte. Authorization: Bearer YOUR_EXPORT_TOKEN måste skickas.
Domain export token är invalid.
Token är wrong, rotated, inte kopplad till denna domain eller domainId är wrong.
Token har inte required scope.
Token har inte required scope. New tokens måste ha articles:read och exports:read.
lang är invalid.
Värdet lang i query parameter skickades incorrectly.

Webhook errors

Webhook URL måste använda HTTPS.
Webhook URL måste börja med https://.
Webhook signature är invalid.
Webhook Secret är wrong, secret har rotated, raw body changed eller signature beräknades incorrectly.
Webhook timestamp är expired eller invalid.
Timestamp är old, ligger i future eller är invalid. Kontrollera server clock och NTP.
Webhook test delivery misslyckades.
Destination website returnerade inte successful status 200 till 299, timed out eller handle inte webhook.test correctly.
STEG 29

Final API och Webhook testing checklist

Efter implementation verify dessa tests connection från beginning till end.

Export Token och JSON test

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

Webhook test

Om du call destination Webhook med Postman utan signature måste du receive security error. Det betyder att endpoint är active och checks security.

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

Klicka sedan på Test Webhook inuti OxiRanker. Om receiver supports webhook.test måste response_status vara 200.

Efter rotation
Om Webhook Secret eller Export Token rotated, copy new value omedelbart, replace den i destination website .env och restart eller redeploy destination backend.
STEG 30

Important rules för correct implementation

Dessa rules måste följas i API integration så att system förblir secure, stable och maintainable.

Export Token måste stored endast i backend.
Webhook Secret måste stored endast i backend.
Destination Webhook endpoint måste vara exakt /api/oxiranker/webhook.
Webhook URL måste använda HTTPS.
Webhook signature och timestamp måste alltid verified.
Articles måste stored med UPSERT, inte simple insert.
Unique constraint krävs för prevent duplicates.
Images måste downloaded från OxiRanker och stored på destination website.
Destination website får inte permanently depend på OxiRanker image URLs.
Image URLs måste replaced i html_body och article_schema.
Om Webhook fails måste daily sync recover missed article.
Errors som shown för users eller logs måste vara English och understandable.
Om article är archived får automatic sync inte publish den igen.
raw_detail_payload och raw_list_item måste preserved så att ingen original output data går förlorad.
Om Export Token eller Webhook Secret rotated måste new value replaced i .env och backend restarted eller redeployed.
STEG 31

Complete API flow summary

Efter att dessa steps slutförts kan din custom website securely receive, store, mirror och publish articles som generated i OxiRanker.

Registrera dig i OxiRanker
Lägg till domain
Verify domain ownership
Välj languages och lägg till Blog URL
Lägg till keywords för varje language
Slutför Article Profile
Konfigurera Telegram vid behov
Konfigurera automatic article engine
Create första article från Generate
Review Preview och SEO Report
Hämta Export Token från API Output
Hämta ready Export API endpoints
Hämta Webhook Secret
Create fixed /api/oxiranker/webhook endpoint i destination backend
Store Token och Secret i destination website .env
Create site_articles table och unique constraint
Implement full article fetching från Export JSON
Implement Image Mirroring
Implement UPSERT
Implement daily sync
Render article, SEO, OG och Schema från destination database
Run final Export API och Webhook tests