OXIRANKER DEVELOPER DOCS

Dokumentasi OxiRanker

Pusat dokumentasi yang kemas untuk menyambungkan laman web anda kepada perkhidmatan OxiRanker. Mulakan dengan integrasi WordPress atau integrasi API, kemudian ikuti panduan langkah demi langkah.

Dokumentasi rasmiFull-feature V1Untuk pemilik laman, pembangun dan pasukan integrasi
Menu dokumentasi
Pilih panduan daripada menu. Halaman ini ialah pusat dokumentasi utama untuk OxiRanker.
Perkhidmatan API

Perkhidmatan API

Gunakan Export API dan Webhook untuk laman web tersuai, platform CMS tersuai, Next.js, Laravel, Node.js, PHP atau backend Python.

Domain
Disahkan
Kandungan
Sedia
API
Webhook
Notis penting penyimpanan

OxiRanker bukan permanent hosting untuk laman web destinasi

OxiRanker tidak menyediakan jaminan permanent hosting untuk kandungan, imej atau fail eksport laman web destinasi. OxiRanker bukan perkhidmatan user-content hosting, dan generated content, cover images, Open Graph images atau output files mungkin tidak kekal di infrastruktur OxiRanker lebih daripada 90 hari.

Oleh itu, selepas menerima output, laman web destinasi mesti menyimpan final article, images, SEO data, Open Graph data dan Schema data dalam database, server, storage atau CDN sendiri, dan tidak boleh bergantung pada OxiRanker URLs untuk paparan awam.

Video latihan

Video latihan sambungan

Gunakan video ini sebagai panduan visual sebelum menyambungkan WordPress atau melaksanakan integrasi API.

API Output

Laksanakan output flow langkah demi langkah

Simpan Export Token dan Webhook Secret dalam backend anda dahulu, kemudian lengkapkan Webhook receiver, database, logik UPSERT, Image Mirroring dan daily sync.

LANGKAH 01

Daftar dan cipta domain dalam OxiRanker

Mula-mula, daftar dalam OxiRanker, sign in ke account anda dan klik Add Domain daripada bahagian Domains.

Format domain yang betul

Dalam medan Domain name, masukkan hanya root domain. Alamat tidak boleh mengandungi http, https atau www.

example.com

Format yang salah

Jangan masukkan format ini dalam medan domain.

https://example.com
http://example.com
www.example.com
Selepas mencipta domain
Jika format domain valid, OxiRanker mencipta domain dan membawa anda ke langkah domain ownership verification.
LANGKAH 02

Sahkan pemilikan domain

Dalam langkah ini, anda mesti membuktikan bahawa domain itu milik anda atau anda mempunyai access untuk menguruskannya.

DNS TXT

Disyorkan

Tambah TXT record pada DNS domain. Untuk Cloudflare, ini biasanya kaedah paling mudah.

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

Letakkan text file dalam path .well-known laman web anda supaya OxiRanker boleh mengesahkannya.

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

File Content:
220daaa37****42730f27

Meta Tag

Letakkan meta tag dalam head section homepage laman web anda.

<meta name="yourdomain-verification" content="190208d****d275b65" />
Nota DNS penting
Selepas menambah DNS record, khususnya dalam Cloudflare, anda mungkin perlu menunggu 2 hingga 3 minit. Kemudian klik Confirm pada halaman OxiRanker yang sama.
Tempoh sah verification token
Domain verification values mempunyai time limit. Pada masa ini, verification values biasanya valid selama 1440 minutes.
LANGKAH 03

Terima token percuma selepas mengesahkan domain pertama

Selepas mengesahkan domain pertama, OxiRanker menambah 1000 free tokens ke account anda untuk initial testing.

Hanya untuk domain pertama
Hadiah ini hanya ditambah untuk domain pertama yang verified dan tidak ditambah semula untuk domain-domain seterusnya.
LANGKAH 04

Konfigurasikan bahasa dan Blog URL

Dalam bahagian Blog URLs, tentukan sama ada laman web anda single-language atau multilingual dan bahasa mana yang perlu digunakan untuk generated articles.

Pilih bahasa

OxiRanker menyokong 24 bahasa. Anda boleh memilih satu atau beberapa bahasa berdasarkan struktur laman web anda.

Masukkan Blog URL

Untuk setiap bahasa, masukkan blog URL bagi bahasa tersebut. Jika laman web anda multilingual, masukkan setiap language path secara berasingan.

https://example.com/fa/blog
https://example.com/en/blog
Struktur URL
Jika laman web anda mempunyai struktur berbeza, masukkan blog URL sebenar laman web anda. Perkara penting ialah final URL mesti menjadi path tempat artikel perlu diterbitkan.
LANGKAH 05

Tambah keyword untuk setiap bahasa

Dalam tab Keywords, tambah sekurang-kurangnya 3 dan maksimum 50 keywords untuk setiap bahasa.

Peraturan utama

Keywords untuk setiap bahasa mesti dimasukkan dalam bahasa yang sama. Jika article language ialah English, keywords juga mesti English.

Cara menambah keywords

Selepas menaip setiap keyword, tekan Enter untuk menambahnya. Selepas melengkapkan semua keywords, klik Save.

Kesilapan biasa
Jika anda memasukkan Turkish keywords untuk English language, Turkish phrases mungkin muncul dalam English content. Keyword language mesti sepadan dengan article language.
LANGKAH 06

Lengkapkan domain Article Profile

Article Profile memberitahu OxiRanker apakah brand anda, untuk siapa ia menulis, tone apa yang perlu digunakan dan topics mana yang perlu dielakkan.

Industry

Mentakrifkan market atau business category. Contoh: real estate consulting dan property buying and selling di Dubai.

Business description

Menerangkan apa yang business jual, siapa yang dilayan, bagaimana ia berfungsi dan apa yang menjadikannya berbeza.

Audience

Mentakrifkan kepada siapa content perlu bercakap. Setiap audience boleh disimpan secara berasingan.

Tone

Mengawal style dan voice generated content, seperti formal, educational, simple, technical atau sales-oriented.

Content goal

Mentakrifkan apa yang artikel perlu capai, seperti menarik customers, meningkatkan traffic atau mendidik users.

Topik dilarang

Apa-apa yang brand tidak patut bincangkan atau tidak patut dikaitkan dengannya dimasukkan di sini.

Optional CTA

CTA bermaksud action yang anda mahu reader lakukan selepas membaca artikel. Jika anda tidak pasti text apa yang suitable, lebih baik biarkan field ini empty.

Multilingual profile
Jika laman web anda multilingual, anda hanya perlu melengkapkan profile dalam satu bahasa. OxiRanker boleh menyediakan versions yang diperlukan untuk bahasa lain berdasarkan profile yang sama.
LANGKAH 07

Pautan dalaman dan luaran

OxiRanker boleh menerapkan internal links dan external links secara intelligent dalam artikel.

Internal links

Option ini enabled secara default dan lebih baik dikekalkan enabled. Internal linking membantu search engines memahami content structure laman web anda dengan lebih baik.

Dalam artikel pertama, internal links biasanya tidak dicipta kerana belum ada previous article. Bermula daripada artikel kedua, system boleh melakukan internal linking.

External links

External links ditambah kepada official dan trusted sources serta ditandakan sebagai nofollow supaya SEO authority tidak dipindahkan secara tidak sengaja.

LANGKAH 08

Semak profile quality dengan AI

Selepas melengkapkan Article Profile, OxiRanker menyemak profile quality.

85+
Minimum score diperlukan untuk save
95
Excellent score yang biasa dicapai selepas improvements
AI
Professional suggestions untuk setiap field
Jika score di bawah 85
System tidak membenarkan final saving dan menunjukkan explanations serta improvement suggestions untuk setiap problematic field. Anda boleh apply suggestions dan save semula.
LANGKAH 09

Sambungkan Telegram untuk article summaries

Selepas profile berjaya save, anda boleh enable penghantaran article summaries ke Telegram channel.

1

Tambah bot

Tambah @Oxiranker_Bot ke Telegram channel anda dan jadikannya Admin.

2

Masukkan channel username

Masukkan channel name dengan @. Contoh: @my_channel

3

Enable summary delivery

Selepas mendaftarkan channel yang valid, enable article summary delivery.

Manfaat Telegram
Jika enabled, selepas content generation, article summary dihantar ke Telegram channel bersama image dan link. Ini boleh menguatkan traffic paths ke laman web anda.
LANGKAH 10

Konfigurasikan automatic article engine

Dalam tab Automatic article generation, anda boleh mengurus automatic article generation engine.

Smart topic selection

System menggunakan keywords dan Article Profile untuk memilih topics yang suitable, unique dan publishable.

Brand-aware generation

Tone, audience, content goal dan brand restrictions dipatuhi semasa article generation.

Scheduled publishing

Monthly capacity diagihkan sepanjang bulan supaya content publishing kelihatan lebih natural dan consistent.

Monthly capacity

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

30 articles bermaksud hampir satu article setiap hari. 15 articles bermaksud hampir selang sehari. 10 articles bermaksud kira-kira setiap dua hingga tiga hari. 7 articles bermaksud kira-kira setiap tiga hingga empat hari.

Article length dan image

Article length boleh menjadi Short, Standard atau Long. Standard ialah balanced choice untuk kebanyakan laman web.

Article images boleh enabled atau disabled, dan image theme boleh menjadi Light atau Dark.

Nota untuk API websites
Automatic engine menjana artikel dalam OxiRanker. Untuk memindahkan setiap article ke custom website anda selepas generation, anda mesti implement Export API, Webhook, database storage dan Image Mirroring.
LANGKAH 11

Cipta artikel pertama daripada Generate

Selepas menyediakan domain, buka menu Generate, pilih domain dan klik New Content atau New Article.

1

Language

Hanya satu language dipilih untuk setiap content request.

2

Topic

Tulis topic yang jelas. Source URL adalah optional.

3

Image

Enable atau disable image dan pilih theme Light atau Dark.

4

Payment dan generation

Cost ditolak daripada wallet dan article generation bermula.

Generation time
Article dan image generation biasanya mengambil masa kira-kira 2 hingga 3 minit. Selepas selesai, article visible dalam OxiRanker.
Source URL
Jika Source URL dimasukkan, OxiRanker menggunakannya untuk memahami topic dengan lebih baik, tetapi ia tidak generate copied content. System direka untuk mencipta unique content.
LANGKAH 12

Semak Preview, SEO Report dan JSON Output

Selepas article generated, beberapa output penting tersedia untuk quality review dan publishing preparation.

Preview

Memaparkan article text dan generated image. Section ini berguna untuk menyemak final quality sebelum publishing.

SEO Report

Memaparkan SEO report yang understandable termasuk SEO Score, title, meta description, canonical, keywords, schema, headings, links, images dan OpenGraph.

JSON Output

Memaparkan full article output untuk technical users, termasuk slug, html_body, schema, image URLs, tags, category, reading time dan created/updated timestamps.

Maksud output
Output ini menunjukkan bahawa article bukan sekadar plain text. Ia ialah structured, SEO-ready content yang disediakan untuk WordPress, CMS, API dan publishing systems.
LANGKAH 13

Apakah sebenarnya fungsi Export API dan Webhook?

Selepas article generated dalam OxiRanker, destination website boleh read article tersebut dengan Export API atau diberitahu melalui Webhook apabila article ready.

Kaedah 1: Export API

Destination website boleh read articles daripada OxiRanker dengan Export Token bila diperlukan. Kaedah ini digunakan untuk manual sync, daily sync, article list pages dan 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

Kaedah 2: Webhook

Apabila new article created atau article updated, OxiRanker notify destination website endpoint. Webhook tidak menghantar full article. Ia hanya menghantar article identifier supaya destination website boleh fetch full article daripada Export JSON API.

https://example.com/api/oxiranker/webhook
{
  "event": "article.created",
  "domain_id": "1",
  "article_id": "5",
  "request_id": "5",
  "lang": "fa",
  "slug": "smart-internal-links",
  "occurred_at": "2026-05-20T19:35:33.000Z"
}
Nota sangat penting
Webhook hanyalah lightweight message. Destination website tidak boleh expect full article dalam Webhook. Selepas menerima domain_id dan article_id, ia mesti fetch complete article daripada Export JSON API dan store dalam database sendiri.
LANGKAH 14

Content storage responsibility dan Image Mirroring

Destination website mesti store final article dan images pada infrastructure sendiri.

Apa yang destination website mesti store

Full article text dalam destination website database
Main article image pada destination server, storage atau CDN
Open Graph image pada destination server, storage atau CDN
SEO, OpenGraph dan Article Schema data
raw_detail_payload dan raw_list_item untuk preserve original output

Contoh image path yang betul

Menggunakan OxiRanker image URLs secara directly dan permanently adalah tidak betul. Image mesti download dan article URLs mesti replace dengan internal URLs destination website.

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker bukan permanent hosting untuk destination website
OxiRanker tidak bertanggungjawab untuk permanently storing output content dan images bagi destination website. OxiRanker bukan user-content hosting service, dan generated content, cover images, Open Graph images atau output files mungkin tidak kekal di OxiRanker infrastructure lebih daripada 90 days. Oleh itu, destination website mesti store final article, images, SEO data, OpenGraph dan Schema dalam database, server, storage atau CDN sendiri.
LANGKAH 15

Dapatkan Export Token dan ready endpoints

Export Token dan ready-to-use endpoints tersedia daripada article page, Outputs tab dan API Output section.

Di mana mendapatkan Export Token

Create article pertama dalam OxiRanker.
Open article page tersebut.
Open Outputs tab.
Open API Output section.
Create token daripada Token management.
Full token hanya shown sekali.
Store token dalam destination website backend atau .env.
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}

Ready-to-use endpoints

Pada article page yang sama, dalam API Output section, Ready-to-use endpoints memaparkan 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
Jika Export Token rotated dalam panel OxiRanker, previous token expires. New value mesti immediately replace old value dalam destination website .env atau backend environment, dan destination backend mesti restarted atau redeployed.
LANGKAH 16

Cipta Webhook Secret dan destination endpoint

Webhook Secret juga dicipta daripada article page, Outputs tab dan API Output section.

Di mana Webhook Secret dicipta?

Open article page.
Open Outputs tab.
Open API Output section.
Create secret dalam Webhook Secret section.
Full secret hanya shown sekali.
Store secret dalam destination website .env.

Fixed destination endpoint

OxiRanker hanya menerima fixed path berikut untuk destination Webhook. Destination website mesti create endpoint ini dalam backend.

POST /api/oxiranker/webhook

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

Endpoint ini mesti hanya accept POST, preserve raw body, verify timestamp dan signature, serta return successful response untuk webhook.test.

Active events
Dalam current version, events adalah fixed: article.created dan article.updated. Untuk testing connection, event value webhook.test dihantar.
LANGKAH 17

Nilai .env diperlukan untuk destination website

OxiRanker tidak mempunyai access kepada destination website files atau .env. Destination website developer mesti store values ini dalam backend sendiri.

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 mesti hanya stored dalam backend.
OXIRANKER_WEBHOOK_SECRET
Webhook Secret mesti hanya stored dalam backend.
OXIRANKER_WEBHOOK_MAX_SKEW_MS
Maximum allowed time difference untuk Webhook. 300000 bermaksud 5 minutes.

Storage values

SITE_ARTICLE_IMAGE_UPLOAD_DIR
Physical path untuk storing mirrored images pada destination server.
SITE_ARTICLE_IMAGE_MAX_BYTES
Maximum allowed image size untuk download dan storage.
PUBLIC_BASE_URL
Public destination website URL yang digunakan untuk create final image dan article URLs.
Frontend security
Export Token dan Webhook Secret tidak boleh diletakkan dalam frontend code, user-visible JavaScript, localStorage atau window object.
LANGKAH 18

Export API: List, JSON, HTML dan ZIP

Destination website boleh menggunakan Export API untuk receive article list atau full output setiap article.

Dapatkan list of articles untuk 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 mentakrifkan number of articles dan kini limited antara 1 dan 50. offset digunakan untuk pagination. lang optional dan digunakan untuk fetch articles bagi specific language seperti fa, en atau tr.

Dapatkan full article sebagai JSON

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

Ini ialah output paling important untuk destination website. Destination website mesti store full article JSON dalam database sendiri dan read daripada database sendiri untuk public display.

Dapatkan 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 sesuai untuk websites yang mahukan ready output, tetapi approach yang lebih professional ialah store JSON dan render dengan template destination website sendiri.

Dapatkan ZIP output

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

ZIP output termasuk article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt dan README.txt.

LANGKAH 19

Webhook Signature dan secure Webhook receiving

Destination website mesti verify Webhook signature menggunakan 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)

Jika destination website parse JSON dahulu dan kemudian stringify semula, order atau spacing mungkin berubah dan signature boleh menjadi invalid. Original raw body mesti digunakan untuk signature verification.

Timing-safe comparison
Jangan bandingkan signatures dengan expected === received. Dalam Node.js, lebih baik gunakan crypto.timingSafeEqual.
LANGKAH 20

Simple Node.js code sample untuk receiving Webhook

Express sample ini handle raw body, timestamp, signature, webhook.test dan 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,
  });
});
LANGKAH 21

Recommended destination website database

Destination website mesti store articles dalam database sendiri dan prevent duplicates dengan unique constraint.

CREATE TABLE public.site_articles (
  id BIGSERIAL PRIMARY KEY,

  source_provider VARCHAR NOT NULL DEFAULT 'oxiranker',
  source_domain_id BIGINT NOT NULL,
  source_article_id BIGINT NOT NULL,
  source_request_id BIGINT NULL,

  lang VARCHAR NOT NULL,
  lang_label VARCHAR NULL,
  dir VARCHAR NOT NULL,
  is_rtl BOOLEAN NULL,

  length VARCHAR NULL,
  topic_text TEXT NULL,

  slug VARCHAR NOT NULL,
  canonical_path VARCHAR NULL,
  effective_canonical_url TEXT NULL,

  title TEXT NOT NULL,
  meta_description TEXT NULL,
  html_body TEXT NULL,

  focus_keywords JSONB NOT NULL DEFAULT '[]'::jsonb,
  category VARCHAR NULL,
  tags JSONB NOT NULL DEFAULT '[]'::jsonb,

  reading_time_minutes INTEGER NULL,

  og_title TEXT NULL,
  og_description TEXT NULL,

  cover_image_url TEXT NULL,
  og_image_url TEXT NULL,

  article_schema JSONB NOT NULL DEFAULT '{}'::jsonb,

  raw_list_item JSONB NOT NULL DEFAULT '{}'::jsonb,
  raw_detail_payload JSONB NOT NULL DEFAULT '{}'::jsonb,

  source_created_at TIMESTAMPTZ NULL,
  source_updated_at TIMESTAMPTZ NULL,

  sync_status VARCHAR NOT NULL DEFAULT 'synced',
  last_synced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  last_sync_error TEXT NULL,

  publish_status VARCHAR NOT NULL DEFAULT 'draft',
  published_at TIMESTAMPTZ NULL,
  deleted_at TIMESTAMPTZ NULL,

  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

  source_cover_image_url TEXT NULL,
  source_og_image_url TEXT NULL,

  image_mirror_status VARCHAR NOT NULL DEFAULT 'pending',
  image_mirror_error TEXT NULL,
  image_mirrored_at TIMESTAMPTZ NULL,

  CONSTRAINT site_articles_dir_check
    CHECK (dir IN ('ltr', 'rtl')),

  CONSTRAINT site_articles_reading_time_check
    CHECK (reading_time_minutes IS NULL OR reading_time_minutes >= 0),

  CONSTRAINT site_articles_sync_status_check
    CHECK (sync_status IN ('pending', 'syncing', 'synced', 'failed')),

  CONSTRAINT site_articles_publish_status_check
    CHECK (publish_status IN ('draft', 'published', 'archived')),

  CONSTRAINT site_articles_image_mirror_status_check
    CHECK (image_mirror_status IN ('pending', 'not_needed', 'mirrored', 'failed')),

  CONSTRAINT site_articles_source_unique
    UNIQUE (source_provider, source_domain_id, source_article_id)
);
CREATE INDEX site_articles_source_lookup_idx
ON public.site_articles (source_provider, source_domain_id, source_article_id);

CREATE INDEX site_articles_source_updated_at_idx
ON public.site_articles (source_updated_at DESC);

CREATE INDEX site_articles_slug_idx
ON public.site_articles (slug)
WHERE deleted_at IS NULL;

CREATE UNIQUE INDEX site_articles_lang_slug_active_unique
ON public.site_articles (lang, slug)
WHERE deleted_at IS NULL;

CREATE INDEX site_articles_lang_publish_idx
ON public.site_articles (lang, publish_status)
WHERE deleted_at IS NULL;

CREATE INDEX site_articles_published_at_idx
ON public.site_articles (published_at DESC)
WHERE deleted_at IS NULL;

CREATE INDEX site_articles_focus_keywords_gin_idx
ON public.site_articles USING gin (focus_keywords);

CREATE INDEX site_articles_tags_gin_idx
ON public.site_articles USING gin (tags);

CREATE INDEX site_articles_article_schema_gin_idx
ON public.site_articles USING gin (article_schema);
Nota PostgreSQL
Dalam PostgreSQL, simple quotes digunakan untuk Persian atau Unicode text. Sesuatu seperti N'...' tidak diperlukan dan tidak boleh digunakan.
LANGKAH 22

Cegah duplicates dengan UPSERT

Article tidak boleh inserted semula untuk setiap Webhook. Jika ia sudah exists, ia mesti updated.

Main key untuk preventing duplicates
Constraint paling important untuk preventing duplicates ialah: 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();
LANGKAH 23

Fetch full article dan sync selepas Webhook

Apabila Webhook tiba, destination website mesti fetch full article daripada Export API, mirror images dan 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,
  });
}
LANGKAH 24

Image Mirroring dan image storage rules

Destination website mesti download images, store pada infrastructure sendiri dan replace article URLs.

Image Mirroring rules

Hanya HTTPS URLs mesti accepted.
File mesti benar-benar image.
content-type mesti bermula dengan image/.
Empty files tidak boleh stored.
File size tidak boleh melebihi limit.
Jika cover dan og sama, download sekali sahaja.
Selepas mirroring, replace URLs dalam html_body dan 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
LANGKAH 25

Daily sync untuk reliability

Webhook sahaja tidak mencukupi. Destination website juga patut mempunyai daily sync untuk recover missed articles.

Mengapa daily sync diperlukan?

Destination website mungkin temporarily down.
Webhook mungkin timeout.
Deployment atau network issues mungkin berlaku.
Article mungkin generated tetapi Webhook-nya missed.

Daily sync algorithm

Start offset daripada 0.
Gunakan limit 20 atau 50.
Fetch article list daripada Export List API.
Untuk setiap item, fetch Export JSON article tersebut.
Mirror images.
Store article dengan UPSERT.
Increase offset dan continue sehingga total completed.
LANGKAH 26

Paparkan articles pada destination website

Selepas storing articles dalam site_articles, destination website tidak boleh connect kepada OxiRanker untuk setiap public display. Ia mesti read daripada database sendiri.

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
Untuk multilingual websites, anda boleh menggunakan pattern /{lang}/blog/{slug}. Contoh: /fa/blog/smart-internal-links
LANGKAH 27

SEO, Schema, RTL dan HTML rendering

Destination website mesti read SEO data daripada database sendiri dan render pada article page.

SEO fields

title untuk page title
meta_description untuk meta description
effective_canonical_url untuk canonical
og_title dan og_description untuk Open Graph
og_image_url atau cover_image_url untuk social preview image
article_schema untuk JSON-LD

HTML rendering

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

Jika destination website mempunyai strict security policy, ia boleh sanitize HTML sebelum saving atau sebelum displaying.

RTL dan LTR

Setiap article mempunyai fields lang, dir dan is_rtl. Jika dir ialah rtl, article page mesti rendered right-to-left.

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

Common errors dan maksudnya

Destination website errors mesti English, clear dan understandable supaya troubleshooting boleh dilakukan dengan cepat.

Export API errors

Domain export token tiada.
Export API token tidak dihantar. Authorization: Bearer YOUR_EXPORT_TOKEN mesti dihantar.
Domain export token tidak valid.
Token salah, rotated, tidak berkaitan dengan domain ini atau domainId salah.
Token tidak mempunyai required scope.
Token tidak mempunyai required scope. New tokens mesti mempunyai articles:read dan exports:read.
lang tidak valid.
Nilai lang dalam query parameter dihantar secara incorrect.

Webhook errors

Webhook URL mesti menggunakan HTTPS.
Webhook URL mesti bermula dengan https://.
Webhook signature tidak valid.
Webhook Secret salah, secret telah rotated, raw body berubah atau signature dikira secara incorrect.
Webhook timestamp expired atau tidak valid.
Timestamp lama, berada di masa depan atau tidak valid. Semak server clock dan NTP.
Webhook test delivery gagal.
Destination website tidak return successful status 200 hingga 299, timed out atau tidak handle webhook.test dengan betul.
LANGKAH 29

Final API dan Webhook testing checklist

Selepas implementation, tests ini verify connection dari beginning hingga end.

Export Token dan 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

Jika anda call destination Webhook dengan Postman tanpa signature, anda mesti receive security error. Ini bermaksud endpoint active dan checks security.

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

Kemudian klik Test Webhook dalam OxiRanker. Jika receiver supports webhook.test, response_status mesti 200.

Selepas rotation
Jika Webhook Secret atau Export Token rotated, copy new value segera, replace dalam destination website .env dan restart atau redeploy destination backend.
LANGKAH 30

Important rules untuk correct implementation

Rules ini mesti diikuti dalam API integration supaya system kekal secure, stable dan maintainable.

Export Token mesti hanya stored dalam backend.
Webhook Secret mesti hanya stored dalam backend.
Destination Webhook endpoint mesti tepat /api/oxiranker/webhook.
Webhook URL mesti menggunakan HTTPS.
Webhook signature dan timestamp mesti sentiasa verified.
Articles mesti stored dengan UPSERT, bukan simple insert.
Unique constraint diperlukan untuk prevent duplicates.
Images mesti downloaded daripada OxiRanker dan stored pada destination website.
Destination website tidak boleh permanently depend pada OxiRanker image URLs.
Image URLs mesti replaced dalam html_body dan article_schema.
Jika Webhook fails, daily sync mesti recover missed article.
Errors yang shown kepada users atau logs mesti English dan understandable.
Jika article archived, automatic sync tidak boleh publish semula.
raw_detail_payload dan raw_list_item mesti preserved supaya tiada original output data hilang.
Jika Export Token atau Webhook Secret rotated, new value mesti replaced dalam .env dan backend mesti restarted atau redeployed.
LANGKAH 31

Complete API flow summary

Selepas melengkapkan steps ini, custom website anda boleh securely receive, store, mirror dan publish articles yang generated dalam OxiRanker.

Daftar dalam OxiRanker
Tambah domain
Sahkan domain ownership
Pilih languages dan tambah Blog URL
Tambah keywords untuk setiap language
Lengkapkan Article Profile
Konfigurasikan Telegram jika perlu
Konfigurasikan automatic article engine
Create article pertama daripada Generate
Semak Preview dan SEO Report
Dapatkan Export Token daripada API Output
Dapatkan ready Export API endpoints
Dapatkan Webhook Secret
Create fixed /api/oxiranker/webhook endpoint dalam destination backend
Store Token dan Secret dalam destination website .env
Create site_articles table dan unique constraint
Implement full article fetching daripada Export JSON
Implement Image Mirroring
Implement UPSERT
Implement daily sync
Render article, SEO, OG dan Schema daripada destination database
Run final Export API dan Webhook tests