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.
Perkhidmatan API
Gunakan Export API dan Webhook untuk laman web tersuai, platform CMS tersuai, Next.js, Laravel, Node.js, PHP atau backend Python.
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 sambungan
Gunakan video ini sebagai panduan visual sebelum menyambungkan WordPress atau melaksanakan integrasi API.
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.
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.comFormat yang salah
Jangan masukkan format ini dalam medan domain.
https://example.com
http://example.com
www.example.comSahkan pemilikan domain
Dalam langkah ini, anda mesti membuktikan bahawa domain itu milik anda atau anda mempunyai access untuk menguruskannya.
DNS TXT
DisyorkanTambah TXT record pada DNS domain. Untuk Cloudflare, ini biasanya kaedah paling mudah.
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP 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****42730f27Meta Tag
Letakkan meta tag dalam head section homepage laman web anda.
<meta name="yourdomain-verification" content="190208d****d275b65" />Terima token percuma selepas mengesahkan domain pertama
Selepas mengesahkan domain pertama, OxiRanker menambah 1000 free tokens ke account anda untuk initial testing.
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/blogTambah 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.
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.
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.
Semak profile quality dengan AI
Selepas melengkapkan Article Profile, OxiRanker menyemak profile quality.
Sambungkan Telegram untuk article summaries
Selepas profile berjaya save, anda boleh enable penghantaran article summaries ke Telegram channel.
Tambah bot
Tambah @Oxiranker_Bot ke Telegram channel anda dan jadikannya Admin.
Masukkan channel username
Masukkan channel name dengan @. Contoh: @my_channel
Enable summary delivery
Selepas mendaftarkan channel yang valid, enable article summary delivery.
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 / month30 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.
Cipta artikel pertama daripada Generate
Selepas menyediakan domain, buka menu Generate, pilih domain dan klik New Content atau New Article.
Language
Hanya satu language dipilih untuk setiap content request.
Topic
Tulis topic yang jelas. Source URL adalah optional.
Image
Enable atau disable image dan pilih theme Light atau Dark.
Payment dan generation
Cost ditolak daripada wallet dan article generation bermula.
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.
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/jsonGET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonKaedah 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"
}Content storage responsibility dan Image Mirroring
Destination website mesti store final article dan images pada infrastructure sendiri.
Apa yang destination website mesti store
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.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpDapatkan 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
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=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=0Cipta Webhook Secret dan destination endpoint
Webhook Secret juga dicipta daripada article page, Outputs tab dan API Output section.
Di mana Webhook Secret dicipta?
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/webhookEndpoint ini mesti hanya accept POST, preserve raw body, verify timestamp dan signature, serta return successful response untuk webhook.test.
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=50Security values
Storage values
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/jsonlimit 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/jsonIni 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_TOKENHTML 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_TOKENZIP output termasuk article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt dan README.txt.
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.
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,
});
});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);Cegah duplicates dengan UPSERT
Article tidak boleh inserted semula untuk setiap Webhook. Jika ia sudah exists, ia mesti updated.
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();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,
});
}Image Mirroring dan image storage rules
Destination website mesti download images, store pada infrastructure sendiri dan replace article URLs.
Image Mirroring rules
Recommended storage path
/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.webpDaily sync untuk reliability
Webhook sahaja tidak mencukupi. Destination website juga patut mempunyai daily sync untuk recover missed articles.
Mengapa daily sync diperlukan?
Daily sync algorithm
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;SEO, Schema, RTL dan HTML rendering
Destination website mesti read SEO data daripada database sendiri dan render pada article page.
SEO fields
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>Common errors dan maksudnya
Destination website errors mesti English, clear dan understandable supaya troubleshooting boleh dilakukan dengan cepat.
Export API errors
Webhook errors
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/jsonGET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonWebhook 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.
Important rules untuk correct implementation
Rules ini mesti diikuti dalam API integration supaya system kekal secure, stable dan maintainable.
Complete API flow summary
Selepas melengkapkan steps ini, custom website anda boleh securely receive, store, mirror dan publish articles yang generated dalam OxiRanker.