เอกสาร OxiRanker
ศูนย์เอกสารที่ชัดเจนสำหรับเชื่อมต่อเว็บไซต์ของคุณกับบริการ OxiRanker เริ่มจากการเชื่อมต่อ WordPress หรือการเชื่อมต่อ API แล้วทำตามคู่มือทีละขั้นตอน
บริการ API
ใช้ Export API และ Webhook สำหรับเว็บไซต์แบบกำหนดเอง แพลตฟอร์ม CMS แบบกำหนดเอง รวมถึง backend ที่ใช้ Next.js, Laravel, Node.js, PHP หรือ Python
OxiRanker ไม่ใช่ permanent hosting สำหรับเว็บไซต์ปลายทาง
OxiRanker ไม่รับประกัน permanent hosting สำหรับเนื้อหา รูปภาพ หรือไฟล์ที่ export ของเว็บไซต์ปลายทาง OxiRanker ไม่ใช่บริการ hosting สำหรับเนื้อหาผู้ใช้ และเนื้อหาที่สร้างขึ้น รูปภาพปก รูปภาพ Open Graph หรือไฟล์ output อาจไม่ถูกเก็บอยู่บน infrastructure ของ OxiRanker เกิน 90 วัน
ดังนั้นหลังจากได้รับ output แล้ว เว็บไซต์ปลายทางต้องจัดเก็บบทความสุดท้าย รูปภาพ ข้อมูล SEO ข้อมูล Open Graph และข้อมูล Schema ไว้ในฐานข้อมูล เซิร์ฟเวอร์ storage หรือ CDN ของตนเอง และไม่ควรพึ่งพา URL ของ OxiRanker สำหรับการแสดงผลสาธารณะ
วิดีโอฝึกอบรมการเชื่อมต่อ
ใช้วิดีโอนี้เป็นคู่มือภาพก่อนเชื่อมต่อ WordPress หรือพัฒนาการเชื่อมต่อ API
พัฒนา flow ของ output ทีละขั้นตอน
บันทึก Export Token และ Webhook Secret ใน backend ของคุณก่อน จากนั้นทำ Webhook receiver, ฐานข้อมูล, ตรรกะ UPSERT, Image Mirroring และ daily sync ให้สมบูรณ์
สมัครและสร้างโดเมนใน OxiRanker
ก่อนอื่น สมัครใช้งาน OxiRanker เข้าสู่บัญชีของคุณ แล้วคลิก Add Domain จากส่วน Domains
รูปแบบโดเมนที่ถูกต้อง
ในช่อง Domain name ให้ใส่เฉพาะ root domain เท่านั้น ที่อยู่ต้องไม่มี http, https หรือ www
example.comรูปแบบที่ไม่ถูกต้อง
อย่าใส่รูปแบบเหล่านี้ในช่องโดเมน
https://example.com
http://example.com
www.example.comยืนยันความเป็นเจ้าของโดเมน
ในขั้นตอนนี้ คุณต้องพิสูจน์ว่าโดเมนเป็นของคุณ หรือคุณมีสิทธิ์เข้าถึงเพื่อจัดการโดเมนนั้น
DNS TXT
แนะนำเพิ่ม TXT record ใน DNS ของโดเมน สำหรับ Cloudflare วิธีนี้มักเป็นวิธีที่ง่ายที่สุด
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP File
วางไฟล์ข้อความไว้ใน path .well-known ของเว็บไซต์ เพื่อให้ OxiRanker สามารถตรวจสอบได้
File URL:
https://example.com/.well-known/yourdomain-verification.txt
File Content:
220daaa37****42730f27Meta Tag
วาง meta tag ไว้ในส่วน head ของหน้าแรกเว็บไซต์ของคุณ
<meta name="yourdomain-verification" content="190208d****d275b65" />รับโทเคนฟรีหลังจากยืนยันโดเมนแรก
หลังจากยืนยันโดเมนแรกแล้ว OxiRanker จะเพิ่ม free tokens จำนวน 1000 เข้าบัญชีของคุณสำหรับ initial testing
ตั้งค่าภาษาและ Blog URL
ในส่วน Blog URLs ให้กำหนดว่าเว็บไซต์ของคุณเป็น single-language หรือ multilingual และควรใช้ภาษาใดสำหรับ generated articles
เลือกภาษา
OxiRanker รองรับ 24 ภาษา คุณสามารถเลือกหนึ่งภาษาหรือหลายภาษาได้ตามโครงสร้างเว็บไซต์ของคุณ
ใส่ Blog URL
สำหรับแต่ละภาษา ให้ใส่ blog URL ของภาษานั้น หากเว็บไซต์ของคุณเป็น multilingual ให้ใส่ language path แต่ละรายการแยกกัน
https://example.com/fa/blog
https://example.com/en/blogเพิ่ม keywords สำหรับแต่ละภาษา
ในแท็บ Keywords ให้เพิ่ม keywords อย่างน้อย 3 และไม่เกิน 50 รายการสำหรับแต่ละภาษา
กฎหลัก
Keywords ของแต่ละภาษาต้องใส่ด้วยภาษาเดียวกัน หาก article language เป็น English keywords ก็ต้องเป็น English ด้วย
วิธีเพิ่ม keywords
หลังจากพิมพ์ keyword แต่ละรายการแล้ว ให้กด Enter เพื่อเพิ่ม เมื่อใส่ครบทุก keywords แล้ว ให้คลิก Save
กรอก Domain Article Profile ให้สมบูรณ์
Article Profile บอก OxiRanker ว่า brand ของคุณคืออะไร เขียนให้ใคร ควรใช้ tone แบบใด และควรหลีกเลี่ยง topics ใด
อุตสาหกรรม
กำหนด market หรือ business category ตัวอย่าง: real estate consulting และ property buying and selling ใน Dubai
คำอธิบายธุรกิจ
อธิบายว่า business ขายอะไร ให้บริการใคร ทำงานอย่างไร และอะไรทำให้แตกต่าง
กลุ่มเป้าหมาย
กำหนดว่า content ควรสื่อสารกับใคร แต่ละ audience สามารถบันทึกแยกกันได้
โทน
ควบคุม style และ voice ของ generated content เช่น formal, educational, simple, technical หรือ sales-oriented
เป้าหมายของเนื้อหา
กำหนดว่า articles ต้องบรรลุอะไร เช่น ดึงดูด customers เพิ่ม traffic หรือให้ความรู้ users
หัวข้อที่ห้ามใช้
ทุกสิ่งที่ brand ไม่ควรพูดถึงหรือไม่ควรถูกเชื่อมโยงด้วย จะถูกใส่ไว้ที่นี่
CTA แบบเลือกได้
CTA หมายถึง action ที่คุณต้องการให้ reader ทำหลังจากอ่าน article หากไม่แน่ใจว่า text ใด suitable ควรปล่อย field นี้ว่างไว้
ลิงก์ภายในและภายนอก
OxiRanker สามารถใส่ internal links และ external links ใน articles ได้อย่าง intelligent
Internal links
ตัวเลือกนี้ enabled เป็น default และควรเปิดไว้ Internal linking ช่วยให้ search engines เข้าใจ content structure ของเว็บไซต์ได้ดีขึ้น
ใน article แรก มักจะยังไม่สร้าง internal links เพราะยังไม่มี previous article ตั้งแต่ article ที่สองเป็นต้นไป system สามารถทำ internal linking ได้
External links
External links จะถูกเพิ่มไปยัง official และ trusted sources และ marked เป็น nofollow เพื่อไม่ให้ SEO authority ถูกส่งต่อโดยไม่ตั้งใจ
ตรวจสอบ profile quality ด้วย AI
หลังจากกรอก Article Profile แล้ว OxiRanker จะ review profile quality
เชื่อมต่อ Telegram สำหรับ article summaries
หลังจาก save profile สำเร็จแล้ว คุณสามารถ enable การส่ง article summaries ไปยัง Telegram channel ได้
เพิ่ม bot
เพิ่ม @Oxiranker_Bot ไปยัง Telegram channel ของคุณและตั้งให้เป็น Admin
ใส่ channel username
ใส่ channel name พร้อม @ ตัวอย่าง: @my_channel
Enable summary delivery
หลังจากลงทะเบียน valid channel แล้ว ให้ enable article summary delivery
ตั้งค่า automatic article engine
ในแท็บ Automatic article generation คุณสามารถจัดการ automatic article generation engine ได้
Smart topic selection
System ใช้ keywords และ Article Profile เพื่อเลือก topics ที่ suitable, unique และ publishable
Brand-aware generation
Tone, audience, content goal และ brand restrictions จะถูกคำนึงถึงระหว่าง article generation
Scheduled publishing
Monthly capacity จะถูกกระจายตลอดทั้งเดือน เพื่อให้ content publishing ดู natural และ consistent มากขึ้น
Monthly capacity
7 articles / month
10 articles / month
15 articles / month
30 articles / month30 articles หมายถึงเกือบหนึ่ง article ต่อวัน 15 articles หมายถึงเกือบวันเว้นวัน 10 articles หมายถึงประมาณทุกสองถึงสามวัน 7 articles หมายถึงประมาณทุกสามถึงสี่วัน
Article length และ image
Article length สามารถเป็น Short, Standard หรือ Long ได้ Standard เป็น balanced choice สำหรับเว็บไซต์ส่วนใหญ่
Article images สามารถ enabled หรือ disabled ได้ และ image theme สามารถเลือกเป็น Light หรือ Dark
สร้าง article แรกจาก Generate
หลังจากเตรียม domain แล้ว ให้เปิด Generate menu เลือก domain แล้วคลิก New Content หรือ New Article
ภาษา
สำหรับแต่ละ content request จะเลือกได้เพียงหนึ่ง language เท่านั้น
หัวข้อ
เขียน topic ให้ชัดเจน Source URL เป็น optional
รูปภาพ
Enable หรือ disable image แล้วเลือก theme เป็น Light หรือ Dark
Payment และ generation
Cost จะถูก deducted จาก wallet แล้ว article generation จะเริ่มต้น
Review Preview, SEO Report และ JSON Output
หลังจาก article generated แล้ว จะมี outputs สำคัญหลายรายการสำหรับ quality review และ publishing preparation
Preview
แสดง article text และ generated image ส่วนนี้มีประโยชน์สำหรับ review final quality ก่อน publishing
SEO Report
แสดง SEO report ที่ understandable รวมถึง SEO Score, title, meta description, canonical, keywords, schema, headings, links, images และ OpenGraph
JSON Output
แสดง full article output สำหรับ technical users รวมถึง slug, html_body, schema, image URLs, tags, category, reading time และ created/updated timestamps
Export API และ Webhook ทำอะไรอย่างชัดเจน?
หลังจาก article ถูก generated ใน OxiRanker แล้ว destination website สามารถ read ด้วย Export API หรือได้รับ notified ผ่าน Webhook เมื่อ article ready
Method 1: Export API
Destination website สามารถ read articles จาก OxiRanker ด้วย Export Token เมื่อจำเป็น method นี้ใช้สำหรับ manual sync, daily sync, article list pages และ 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/jsonMethod 2: Webhook
เมื่อ new article created หรือ article updated, OxiRanker จะ notify destination website endpoint Webhook ไม่ส่ง full article แต่ส่งเฉพาะ article identifier เพื่อให้ destination website fetch full article จาก 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 และ Image Mirroring
Destination website ต้อง store final article และ images บน infrastructure ของตนเอง
สิ่งที่ destination website ต้อง store
ตัวอย่าง image path ที่ถูกต้อง
การใช้ OxiRanker image URLs โดย directly และ permanently ไม่ถูกต้อง ต้อง download image และ replace article URLs ด้วย internal URLs ของ destination website
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpรับ Export Token และ ready endpoints
Export Token และ ready-to-use endpoints มีให้ใน article page, Outputs tab และ API Output section
รับ Export Token จากที่ไหน
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}Ready-to-use endpoints
ใน article page เดียวกัน ภายใน API Output section, Ready-to-use endpoints จะแสดง 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=0สร้าง Webhook Secret และ destination endpoint
Webhook Secret ถูกสร้างจาก article page, Outputs tab และ API Output section เช่นกัน
Webhook Secret สร้างที่ไหน?
Fixed destination endpoint
OxiRanker รับเฉพาะ fixed path ต่อไปนี้สำหรับ destination Webhook เท่านั้น Destination website ต้อง create endpoint นี้ใน backend ของตนเอง
POST /api/oxiranker/webhook
https://example.com/api/oxiranker/webhookEndpoint นี้ต้อง accept เฉพาะ POST, preserve raw body, verify timestamp และ signature และ return successful response สำหรับ webhook.test
Required .env values สำหรับ destination website
OxiRanker ไม่มี access ไปยัง destination website files หรือ .env ผู้พัฒนา destination website ต้อง store values เหล่านี้ไว้ใน 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=50Security values
Storage values
Export API: List, JSON, HTML และ ZIP
Destination website สามารถใช้ Export API เพื่อ receive article list หรือ full output ของแต่ละ article ได้
รับ list of articles สำหรับ domain
GET https://oxiranker.com/api/v1/users/domains/:domainId/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonlimit กำหนด number of articles และปัจจุบัน limited ระหว่าง 1 ถึง 50 offset ใช้สำหรับ pagination lang เป็น optional และใช้ fetch articles สำหรับ specific language เช่น fa, en หรือ tr
รับ full article เป็น JSON
GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonนี่คือ output ที่ important ที่สุดสำหรับ destination website Destination website ต้อง store full article JSON ใน database ของตนเอง และ read จาก database ของตนเองสำหรับ public display
รับ 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 เหมาะสำหรับ websites ที่ต้องการ ready output แต่ approach ที่ professional กว่าคือ store JSON และ render ด้วย template ของ destination website เอง
รับ ZIP output
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/zip
Authorization: Bearer YOUR_EXPORT_TOKENZIP output ประกอบด้วย article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt และ README.txt
Webhook Signature และ secure Webhook receiving
Destination website ต้อง verify Webhook signature โดยใช้ 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)หาก destination website parse JSON ก่อนแล้ว stringify อีกครั้ง order หรือ spacing อาจเปลี่ยน และ signature อาจ invalid ต้องใช้ original raw body สำหรับ signature verification
Simple Node.js code sample สำหรับ receiving Webhook
Express sample นี้ handle raw body, timestamp, signature, webhook.test และ 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 ต้อง store articles ใน database ของตนเอง และ prevent duplicates ด้วย 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);Prevent duplicates ด้วย UPSERT
Article ไม่ควรถูก inserted ซ้ำสำหรับทุก Webhook หากมีอยู่แล้ว ต้อง 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 และ sync หลัง Webhook
เมื่อ Webhook มาถึง destination website ต้อง fetch full article จาก Export API, mirror images และ 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 และ image storage rules
Destination website ต้อง download images, store บน infrastructure ของตนเอง และ 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 เพื่อ reliability
Webhook alone ไม่เพียงพอ Destination website ควรมี daily sync เพื่อ recover missed articles ด้วย
ทำไมต้องมี daily sync?
Daily sync algorithm
แสดง articles บน destination website
หลังจาก storing articles ใน site_articles แล้ว destination website ไม่ควร connect ไปยัง OxiRanker สำหรับทุก public display แต่ต้อง read จาก 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;SEO, Schema, RTL และ HTML rendering
Destination website ต้อง read SEO data จาก database ของตนเอง และ render บน article page
SEO fields
HTML rendering
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.html_body }}
/>หาก destination website มี strict security policy สามารถ sanitize HTML ก่อน saving หรือก่อน displaying ได้
RTL และ LTR
ทุก article มี fields lang, dir และ is_rtl หาก dir เป็น rtl, article page ต้อง rendered แบบ right-to-left
<article dir="rtl">
...
</article>Common errors และความหมาย
Destination website errors ต้องเป็น English, clear และ understandable เพื่อให้ troubleshooting ทำได้เร็ว
Export API errors
Webhook errors
Final API และ Webhook testing checklist
หลัง implementation แล้ว tests เหล่านี้จะ verify connection ตั้งแต่ beginning ถึง end
Export Token และ 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
หากคุณ call destination Webhook ด้วย Postman โดยไม่มี signature คุณต้อง receive security error สิ่งนี้หมายความว่า endpoint active และ checks security
{
"error": "missing_webhook_signature",
"message": "Webhook signature header is required."
}จากนั้นคลิก Test Webhook ภายใน OxiRanker หาก receiver supports webhook.test, response_status ต้องเป็น 200
Important rules สำหรับ correct implementation
Rules เหล่านี้ต้องถูกปฏิบัติตามใน API integration เพื่อให้ system ยังคง secure, stable และ maintainable
Complete API flow summary
หลังจากทำ steps เหล่านี้เสร็จ custom website ของคุณจะสามารถ securely receive, store, mirror และ publish articles ที่ generated ใน OxiRanker ได้