เอกสารสำหรับนักพัฒนา OXIRANKER

เอกสาร OxiRanker

ศูนย์เอกสารที่ชัดเจนสำหรับเชื่อมต่อเว็บไซต์ของคุณกับบริการ OxiRanker เริ่มจากการเชื่อมต่อ WordPress หรือการเชื่อมต่อ API แล้วทำตามคู่มือทีละขั้นตอน

เอกสารอย่างเป็นทางการFull-feature V1สำหรับเจ้าของเว็บไซต์ นักพัฒนา และทีมเชื่อมต่อระบบ
เมนูเอกสาร
เลือกคู่มือจากเมนู หน้านี้คือศูนย์เอกสารหลักของ OxiRanker
บริการ API

บริการ API

ใช้ Export API และ Webhook สำหรับเว็บไซต์แบบกำหนดเอง แพลตฟอร์ม CMS แบบกำหนดเอง รวมถึง backend ที่ใช้ Next.js, Laravel, Node.js, PHP หรือ Python

โดเมน
ยืนยันแล้ว
เนื้อหา
พร้อม
API
Webhook
ประกาศสำคัญเกี่ยวกับการจัดเก็บ

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

API Output

พัฒนา flow ของ output ทีละขั้นตอน

บันทึก Export Token และ Webhook Secret ใน backend ของคุณก่อน จากนั้นทำ Webhook receiver, ฐานข้อมูล, ตรรกะ UPSERT, Image Mirroring และ daily sync ให้สมบูรณ์

ขั้นตอน 01

สมัครและสร้างโดเมนใน OxiRanker

ก่อนอื่น สมัครใช้งาน OxiRanker เข้าสู่บัญชีของคุณ แล้วคลิก Add Domain จากส่วน Domains

รูปแบบโดเมนที่ถูกต้อง

ในช่อง Domain name ให้ใส่เฉพาะ root domain เท่านั้น ที่อยู่ต้องไม่มี http, https หรือ www

example.com

รูปแบบที่ไม่ถูกต้อง

อย่าใส่รูปแบบเหล่านี้ในช่องโดเมน

https://example.com
http://example.com
www.example.com
หลังจากสร้างโดเมน
หากรูปแบบโดเมนถูกต้อง OxiRanker จะสร้างโดเมนและพาคุณไปยังขั้นตอนยืนยันความเป็นเจ้าของโดเมน
ขั้นตอน 02

ยืนยันความเป็นเจ้าของโดเมน

ในขั้นตอนนี้ คุณต้องพิสูจน์ว่าโดเมนเป็นของคุณ หรือคุณมีสิทธิ์เข้าถึงเพื่อจัดการโดเมนนั้น

DNS TXT

แนะนำ

เพิ่ม TXT record ใน DNS ของโดเมน สำหรับ Cloudflare วิธีนี้มักเป็นวิธีที่ง่ายที่สุด

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

วางไฟล์ข้อความไว้ใน path .well-known ของเว็บไซต์ เพื่อให้ OxiRanker สามารถตรวจสอบได้

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

File Content:
220daaa37****42730f27

Meta Tag

วาง meta tag ไว้ในส่วน head ของหน้าแรกเว็บไซต์ของคุณ

<meta name="yourdomain-verification" content="190208d****d275b65" />
หมายเหตุ DNS สำคัญ
หลังจากเพิ่ม DNS record แล้ว โดยเฉพาะใน Cloudflare คุณอาจต้องรอ 2 ถึง 3 นาที จากนั้นคลิก Confirm บนหน้า OxiRanker เดิม
อายุของ verification token
ค่าการยืนยันโดเมนมี time limit ปัจจุบันค่าการยืนยันมัก valid เป็นเวลา 1440 minutes
ขั้นตอน 03

รับโทเคนฟรีหลังจากยืนยันโดเมนแรก

หลังจากยืนยันโดเมนแรกแล้ว OxiRanker จะเพิ่ม free tokens จำนวน 1000 เข้าบัญชีของคุณสำหรับ initial testing

สำหรับโดเมนแรกเท่านั้น
ของขวัญนี้จะถูกเพิ่มให้เฉพาะโดเมนแรกที่ verified แล้วเท่านั้น และจะไม่ถูกเพิ่มซ้ำสำหรับโดเมนถัดไป
ขั้นตอน 04

ตั้งค่าภาษาและ 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
โครงสร้าง URL
หากเว็บไซต์ของคุณมีโครงสร้างต่างออกไป ให้ใส่ blog URL จริงของเว็บไซต์คุณ จุดสำคัญคือ final URL ต้องเป็น path ที่บทความจะถูกเผยแพร่
ขั้นตอน 05

เพิ่ม keywords สำหรับแต่ละภาษา

ในแท็บ Keywords ให้เพิ่ม keywords อย่างน้อย 3 และไม่เกิน 50 รายการสำหรับแต่ละภาษา

กฎหลัก

Keywords ของแต่ละภาษาต้องใส่ด้วยภาษาเดียวกัน หาก article language เป็น English keywords ก็ต้องเป็น English ด้วย

วิธีเพิ่ม keywords

หลังจากพิมพ์ keyword แต่ละรายการแล้ว ให้กด Enter เพื่อเพิ่ม เมื่อใส่ครบทุก keywords แล้ว ให้คลิก Save

ข้อผิดพลาดที่พบบ่อย
หากคุณใส่ Turkish keywords สำหรับ English language อาจมี Turkish phrases ปรากฏใน English content ได้ ภาษา keyword ต้องตรงกับ article language
ขั้นตอน 06

กรอก 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 นี้ว่างไว้

โปรไฟล์หลายภาษา
หากเว็บไซต์ของคุณเป็น multilingual คุณต้องกรอก profile เพียงภาษาเดียว OxiRanker สามารถเตรียม versions ที่จำเป็นสำหรับภาษาอื่นจาก profile เดียวกันได้
ขั้นตอน 07

ลิงก์ภายในและภายนอก

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 ถูกส่งต่อโดยไม่ตั้งใจ

ขั้นตอน 08

ตรวจสอบ profile quality ด้วย AI

หลังจากกรอก Article Profile แล้ว OxiRanker จะ review profile quality

85+
Minimum score ที่จำเป็นสำหรับการ save
95
Excellent score ที่มักได้หลัง improvements
AI
Professional suggestions สำหรับแต่ละ field
หาก score ต่ำกว่า 85
System จะไม่อนุญาตให้ final saving และจะแสดง explanations พร้อม improvement suggestions สำหรับแต่ละ problematic field คุณสามารถ apply suggestions แล้ว save อีกครั้งได้
ขั้นตอน 09

เชื่อมต่อ Telegram สำหรับ article summaries

หลังจาก save profile สำเร็จแล้ว คุณสามารถ enable การส่ง article summaries ไปยัง Telegram channel ได้

1

เพิ่ม bot

เพิ่ม @Oxiranker_Bot ไปยัง Telegram channel ของคุณและตั้งให้เป็น Admin

2

ใส่ channel username

ใส่ channel name พร้อม @ ตัวอย่าง: @my_channel

3

Enable summary delivery

หลังจากลงทะเบียน valid channel แล้ว ให้ enable article summary delivery

ประโยชน์ของ Telegram
หาก enabled หลัง content generation แล้ว article summary จะถูกส่งไปยัง Telegram channel พร้อม image และ link ซึ่งช่วยเสริม traffic paths ไปยังเว็บไซต์ของคุณได้
ขั้นตอน 10

ตั้งค่า 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 / month

30 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

หมายเหตุสำหรับ API websites
Automatic engine สร้าง articles ภายใน OxiRanker หากต้องการย้ายแต่ละ article ไปยัง custom website หลัง generation คุณต้อง implement Export API, Webhook, database storage และ Image Mirroring
ขั้นตอน 11

สร้าง article แรกจาก Generate

หลังจากเตรียม domain แล้ว ให้เปิด Generate menu เลือก domain แล้วคลิก New Content หรือ New Article

1

ภาษา

สำหรับแต่ละ content request จะเลือกได้เพียงหนึ่ง language เท่านั้น

2

หัวข้อ

เขียน topic ให้ชัดเจน Source URL เป็น optional

3

รูปภาพ

Enable หรือ disable image แล้วเลือก theme เป็น Light หรือ Dark

4

Payment และ generation

Cost จะถูก deducted จาก wallet แล้ว article generation จะเริ่มต้น

Generation time
Article และ image generation โดยปกติใช้เวลาประมาณ 2 ถึง 3 นาที หลังเสร็จแล้ว article จะ visible ภายใน OxiRanker
Source URL
หากใส่ Source URL แล้ว OxiRanker จะใช้เพื่อเข้าใจ topic ได้ดีขึ้น แต่จะไม่ generate copied content ระบบถูก designed เพื่อสร้าง unique content
ขั้นตอน 12

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

ความหมายของ outputs
outputs เหล่านี้แสดงว่า article ไม่ใช่เพียง plain text แต่เป็น structured, SEO-ready content ที่เตรียมไว้สำหรับ WordPress, CMS, API และ publishing systems
ขั้นตอน 13

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/json
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Method 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"
}
หมายเหตุสำคัญมาก
Webhook เป็นเพียง lightweight message เท่านั้น Destination website ไม่ควร expect full article ภายใน Webhook หลังจากได้รับ domain_id และ article_id แล้ว ต้อง fetch complete article จาก Export JSON API และ store ไว้ใน database ของตนเอง
ขั้นตอน 14

Content storage responsibility และ Image Mirroring

Destination website ต้อง store final article และ images บน infrastructure ของตนเอง

สิ่งที่ destination website ต้อง store

Full article text ใน destination website database
Main article image บน destination server, storage หรือ CDN
Open Graph image บน destination server, storage หรือ CDN
SEO, OpenGraph และ Article Schema data
raw_detail_payload และ raw_list_item เพื่อ preserve original output

ตัวอย่าง image path ที่ถูกต้อง

การใช้ OxiRanker image URLs โดย directly และ permanently ไม่ถูกต้อง ต้อง download image และ replace article URLs ด้วย 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 ไม่ใช่ permanent hosting สำหรับ destination website
OxiRanker ไม่รับผิดชอบต่อ permanently storing output content และ images สำหรับ destination website OxiRanker ไม่ใช่ user-content hosting service และ generated content, cover images, Open Graph images หรือ output files อาจไม่อยู่บน OxiRanker infrastructure เกิน 90 days ดังนั้น destination website ต้อง store final article, images, SEO data, OpenGraph และ Schema ใน database, server, storage หรือ CDN ของตนเอง
ขั้นตอน 15

รับ Export Token และ ready endpoints

Export Token และ ready-to-use endpoints มีให้ใน article page, Outputs tab และ API Output section

รับ Export Token จากที่ไหน

Create article แรกใน OxiRanker
Open article page นั้น
Open Outputs tab
Open API Output section
Create token จาก Token management
Full token shown เพียงครั้งเดียว
Store token ใน destination website backend หรือ .env
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=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
หาก Export Token rotated ใน OxiRanker panel, previous token จะ expires New value ต้อง immediately replace old value ใน destination website .env หรือ backend environment และ destination backend ต้อง restarted หรือ redeployed
ขั้นตอน 16

สร้าง Webhook Secret และ destination endpoint

Webhook Secret ถูกสร้างจาก article page, Outputs tab และ API Output section เช่นกัน

Webhook Secret สร้างที่ไหน?

Open article page
Open Outputs tab
Open API Output section
Create secret ใน Webhook Secret section
Full secret shown เพียงครั้งเดียว
Store secret ใน destination website .env

Fixed destination endpoint

OxiRanker รับเฉพาะ fixed path ต่อไปนี้สำหรับ destination Webhook เท่านั้น Destination website ต้อง create endpoint นี้ใน backend ของตนเอง

POST /api/oxiranker/webhook

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

Endpoint นี้ต้อง accept เฉพาะ POST, preserve raw body, verify timestamp และ signature และ return successful response สำหรับ webhook.test

Active events
ใน current version, events เป็น fixed: article.created และ article.updated สำหรับ testing connection จะส่ง event value webhook.test
ขั้นตอน 17

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=50

Security values

OXIRANKER_EXPORT_API_TOKEN
Export API token ต้อง stored เฉพาะใน backend เท่านั้น
OXIRANKER_WEBHOOK_SECRET
Webhook Secret ต้อง stored เฉพาะใน backend เท่านั้น
OXIRANKER_WEBHOOK_MAX_SKEW_MS
Maximum allowed time difference สำหรับ Webhook 300000 หมายถึง 5 minutes

Storage values

SITE_ARTICLE_IMAGE_UPLOAD_DIR
Physical path สำหรับ storing mirrored images บน destination server
SITE_ARTICLE_IMAGE_MAX_BYTES
Maximum allowed image size สำหรับ download และ storage
PUBLIC_BASE_URL
Public destination website URL ที่ใช้ create final image และ article URLs
Frontend security
Export Token และ Webhook Secret ห้ามถูกวางไว้ใน frontend code, user-visible JavaScript, localStorage หรือ window object
ขั้นตอน 18

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/json

limit กำหนด 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_TOKEN

HTML เหมาะสำหรับ 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_TOKEN

ZIP output ประกอบด้วย article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt และ README.txt

ขั้นตอน 19

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

Timing-safe comparison
อย่าเปรียบเทียบ signatures ด้วย expected === received ใน Node.js ควรใช้ crypto.timingSafeEqual
ขั้นตอน 20

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,
  });
});
ขั้นตอน 21

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);
PostgreSQL note
ใน PostgreSQL ใช้ simple quotes สำหรับ Persian หรือ Unicode text สิ่งที่คล้าย N'...' ไม่จำเป็นและไม่ควรใช้
ขั้นตอน 22

Prevent duplicates ด้วย UPSERT

Article ไม่ควรถูก inserted ซ้ำสำหรับทุก Webhook หากมีอยู่แล้ว ต้อง updated

Main key สำหรับ preventing duplicates
Constraint ที่ important ที่สุดสำหรับ preventing duplicates คือ: 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();
ขั้นตอน 23

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,
  });
}
ขั้นตอน 24

Image Mirroring และ image storage rules

Destination website ต้อง download images, store บน infrastructure ของตนเอง และ replace article URLs

Image Mirroring rules

ต้อง accepted เฉพาะ HTTPS URLs เท่านั้น
File ต้องเป็น image จริง
content-type ต้องขึ้นต้นด้วย image/
Empty files ห้าม stored
File size ต้องไม่เกิน limit
หาก cover และ og เหมือนกัน ให้ download เพียงครั้งเดียว
หลัง mirroring ให้ replace URLs ใน html_body และ 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
ขั้นตอน 25

Daily sync เพื่อ reliability

Webhook alone ไม่เพียงพอ Destination website ควรมี daily sync เพื่อ recover missed articles ด้วย

ทำไมต้องมี daily sync?

Destination website อาจ temporarily down
Webhook อาจ timeout
Deployment หรือ network issues อาจเกิดขึ้น
Article อาจ generated แล้ว แต่ Webhook ของมันอาจ missed

Daily sync algorithm

Start offset จาก 0
ใช้ limit 20 หรือ 50
Fetch article list จาก Export List API
สำหรับแต่ละ item ให้ fetch Export JSON ของ article นั้น
Mirror images
Store article ด้วย UPSERT
Increase offset และ continue จนกว่า total จะ completed
ขั้นตอน 26

แสดง 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;
Recommended article URL
สำหรับ multilingual websites คุณสามารถใช้ pattern /{lang}/blog/{slug} ตัวอย่าง: /fa/blog/smart-internal-links
ขั้นตอน 27

SEO, Schema, RTL และ HTML rendering

Destination website ต้อง read SEO data จาก database ของตนเอง และ render บน article page

SEO fields

title สำหรับ page title
meta_description สำหรับ meta description
effective_canonical_url สำหรับ canonical
og_title และ og_description สำหรับ Open Graph
og_image_url หรือ cover_image_url สำหรับ social preview image
article_schema สำหรับ JSON-LD

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>
ขั้นตอน 28

Common errors และความหมาย

Destination website errors ต้องเป็น English, clear และ understandable เพื่อให้ troubleshooting ทำได้เร็ว

Export API errors

ไม่มี Domain export token
ไม่ได้ส่ง Export API token ต้องส่ง Authorization: Bearer YOUR_EXPORT_TOKEN
Domain export token invalid
Token wrong, rotated, ไม่เกี่ยวข้องกับ domain นี้ หรือ domainId wrong
Token ไม่มี required scope
Token ไม่มี required scope New tokens ต้องมี articles:read และ exports:read
lang invalid
ค่า lang ใน query parameter ถูกส่งมา incorrectly

Webhook errors

Webhook URL ต้องใช้ HTTPS
Webhook URL ต้องขึ้นต้นด้วย https://
Webhook signature invalid
Webhook Secret wrong, secret rotated, raw body changed หรือ signature คำนวณ incorrectly
Webhook timestamp expired หรือ invalid
Timestamp old, อยู่ใน future หรือ invalid ตรวจสอบ server clock และ NTP
Webhook test delivery ล้มเหลว
Destination website ไม่ return successful status 200 ถึง 299, timed out หรือ handle webhook.test ไม่ correctly
ขั้นตอน 29

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/json
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Webhook 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

หลัง rotation
หาก Webhook Secret หรือ Export Token rotated ให้ copy new value ทันที replace ใน destination website .env แล้ว restart หรือ redeploy destination backend
ขั้นตอน 30

Important rules สำหรับ correct implementation

Rules เหล่านี้ต้องถูกปฏิบัติตามใน API integration เพื่อให้ system ยังคง secure, stable และ maintainable

Export Token ต้อง stored เฉพาะใน backend เท่านั้น
Webhook Secret ต้อง stored เฉพาะใน backend เท่านั้น
Destination Webhook endpoint ต้องเป็น /api/oxiranker/webhook อย่างถูกต้องเท่านั้น
Webhook URL ต้องใช้ HTTPS
Webhook signature และ timestamp ต้อง verified เสมอ
Articles ต้อง stored ด้วย UPSERT ไม่ใช่ simple insert
ต้องมี unique constraint เพื่อ prevent duplicates
Images ต้อง downloaded จาก OxiRanker และ stored บน destination website
Destination website ต้องไม่ permanently depend กับ OxiRanker image URLs
Image URLs ต้อง replaced ใน html_body และ article_schema
หาก Webhook fails, daily sync ต้อง recover missed article
Errors ที่ shown ให้ users หรือ logs ต้องเป็น English และ understandable
หาก article archived, automatic sync ต้องไม่ publish อีกครั้ง
raw_detail_payload และ raw_list_item ต้อง preserved เพื่อไม่ให้ original output data สูญหาย
หาก Export Token หรือ Webhook Secret rotated, new value ต้อง replaced ใน .env และ backend ต้อง restarted หรือ redeployed
ขั้นตอน 31

Complete API flow summary

หลังจากทำ steps เหล่านี้เสร็จ custom website ของคุณจะสามารถ securely receive, store, mirror และ publish articles ที่ generated ใน OxiRanker ได้

สมัครใน OxiRanker
เพิ่ม domain
Verify domain ownership
เลือก languages และเพิ่ม Blog URL
เพิ่ม keywords สำหรับแต่ละ language
กรอก Article Profile ให้สมบูรณ์
ตั้งค่า Telegram หากจำเป็น
ตั้งค่า automatic article engine
Create article แรกจาก Generate
Review Preview และ SEO Report
รับ Export Token จาก API Output
รับ ready Export API endpoints
รับ Webhook Secret
Create fixed /api/oxiranker/webhook endpoint ใน destination backend
Store Token และ Secret ใน destination website .env
Create site_articles table และ unique constraint
Implement full article fetching จาก Export JSON
Implement Image Mirroring
Implement UPSERT
Implement daily sync
Render article, SEO, OG และ Schema จาก destination database
Run final Export API และ Webhook tests