Τεκμηρίωση OxiRanker
Ένα καθαρό κέντρο τεκμηρίωσης για τη σύνδεση του ιστοτόπου σας με τις υπηρεσίες OxiRanker. Ξεκινήστε με την ενσωμάτωση WordPress ή την ενσωμάτωση API και ακολουθήστε τον οδηγό βήμα προς βήμα.
Υπηρεσία API
Χρησιμοποιήστε Export API και Webhook για προσαρμοσμένους ιστοτόπους, προσαρμοσμένες πλατφόρμες CMS, Next.js, Laravel, Node.js, PHP ή Python backends.
Το OxiRanker δεν είναι μόνιμη φιλοξενία για τον ιστότοπο προορισμού
Το OxiRanker δεν παρέχει εγγύηση μόνιμης φιλοξενίας για το περιεχόμενο, τις εικόνες ή τα εξαγόμενα αρχεία του ιστοτόπου προορισμού. Το OxiRanker δεν είναι υπηρεσία φιλοξενίας περιεχομένου χρηστών και το δημιουργημένο περιεχόμενο, οι εικόνες εξωφύλλου, οι εικόνες Open Graph ή τα αρχεία εξόδου ενδέχεται να μην παραμείνουν στην υποδομή του OxiRanker για περισσότερο από 90 ημέρες.
Επομένως, μετά τη λήψη της εξόδου, ο ιστότοπος προορισμού πρέπει να αποθηκεύσει το τελικό άρθρο, τις εικόνες, τα δεδομένα SEO, τα δεδομένα Open Graph και τα δεδομένα Schema στη δική του βάση δεδομένων, διακομιστή, αποθηκευτικό χώρο ή CDN και δεν πρέπει να βασίζεται σε URL του OxiRanker για δημόσια εμφάνιση.
Εκπαιδευτικό βίντεο σύνδεσης
Χρησιμοποιήστε αυτό το βίντεο ως οπτική καθοδήγηση πριν συνδέσετε το WordPress ή υλοποιήσετε την ενσωμάτωση API.
Υλοποιήστε τη ροή εξόδου βήμα προς βήμα
Αποθηκεύστε πρώτα το Export Token και το Webhook Secret στο backend σας και έπειτα ολοκληρώστε τον Webhook receiver, τη βάση δεδομένων, τη λογική UPSERT, το Image Mirroring και το ημερήσιο sync.
Εγγραφή και δημιουργία domain στο OxiRanker
Πρώτα, εγγραφείτε στο OxiRanker, συνδεθείτε στον λογαριασμό σας και κάντε κλικ στο Add Domain από την ενότητα Domains.
Σωστή μορφή domain
Στο πεδίο Domain name, εισαγάγετε μόνο το root domain. Η διεύθυνση δεν πρέπει να περιλαμβάνει http, https ή www.
example.comΛανθασμένες μορφές
Μην εισαγάγετε αυτές τις μορφές στο πεδίο domain.
https://example.com
http://example.com
www.example.comΕπαλήθευση ιδιοκτησίας domain
Σε αυτό το βήμα, πρέπει να αποδείξετε ότι το domain σας ανήκει ή ότι έχετε πρόσβαση για τη διαχείρισή του.
DNS TXT
ΠροτείνεταιΠροσθέστε ένα TXT record στο DNS του domain. Για το Cloudflare, αυτή είναι συνήθως η πιο απλή μέθοδος.
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP File
Τοποθετήστε ένα αρχείο κειμένου στη διαδρομή .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" />Λήψη δωρεάν tokens μετά την επαλήθευση του πρώτου domain
Μετά την επαλήθευση του πρώτου domain, το OxiRanker προσθέτει 1000 δωρεάν tokens στον λογαριασμό σας για αρχικές δοκιμές.
Ρύθμιση γλωσσών και Blog URL
Στην ενότητα Blog URLs, ορίστε αν ο ιστότοπός σας είναι μονόγλωσσος ή πολύγλωσσος και ποιες γλώσσες πρέπει να χρησιμοποιούνται για τα δημιουργημένα άρθρα.
Επιλογή γλωσσών
Το OxiRanker υποστηρίζει 24 γλώσσες. Μπορείτε να επιλέξετε μία ή περισσότερες γλώσσες ανάλογα με τη δομή του ιστοτόπου σας.
Εισαγωγή Blog URL
Για κάθε γλώσσα, εισαγάγετε το Blog URL της συγκεκριμένης γλώσσας. Αν ο ιστότοπός σας είναι πολύγλωσσος, εισαγάγετε ξεχωριστά τη διαδρομή κάθε γλώσσας.
https://example.com/fa/blog
https://example.com/en/blogΠροσθήκη λέξεων-κλειδιών για κάθε γλώσσα
Στην καρτέλα Keywords, προσθέστε τουλάχιστον 3 και έως 50 λέξεις-κλειδιά για κάθε γλώσσα.
Βασικός κανόνας
Οι λέξεις-κλειδιά για κάθε γλώσσα πρέπει να εισάγονται στην ίδια γλώσσα. Αν η γλώσσα του άρθρου είναι αγγλικά, οι λέξεις-κλειδιά πρέπει επίσης να είναι στα αγγλικά.
Πώς να προσθέσετε λέξεις-κλειδιά
Αφού πληκτρολογήσετε κάθε λέξη-κλειδί, πατήστε Enter για να την προσθέσετε. Αφού ολοκληρώσετε όλες τις λέξεις-κλειδιά, κάντε κλικ στο Save.
Ολοκλήρωση του Article Profile του domain
Το Article Profile λέει στο OxiRanker τι είναι το brand σας, για ποιον γράφει, ποιον τόνο πρέπει να χρησιμοποιεί και ποια θέματα πρέπει να αποφεύγει.
Κλάδος
Ορίζει την αγορά ή την επιχειρηματική κατηγορία. Παράδειγμα: συμβουλευτική ακινήτων και αγοραπωλησίες ακινήτων στο Ντουμπάι.
Περιγραφή επιχείρησης
Εξηγεί τι πουλά η επιχείρηση, ποιον εξυπηρετεί, πώς λειτουργεί και τι την κάνει διαφορετική.
Κοινό
Ορίζει σε ποιο κοινό πρέπει να απευθύνεται το περιεχόμενο. Κάθε κοινό μπορεί να αποθηκευτεί ξεχωριστά.
Τόνος
Ελέγχει το ύφος και τη φωνή του δημιουργημένου περιεχομένου, όπως επίσημο, εκπαιδευτικό, απλό, τεχνικό ή προσανατολισμένο στις πωλήσεις.
Στόχος περιεχομένου
Ορίζει τι πρέπει να πετυχαίνουν τα άρθρα, όπως προσέλκυση πελατών, αύξηση επισκεψιμότητας ή εκπαίδευση χρηστών.
Απαγορευμένα θέματα
Οτιδήποτε δεν πρέπει να συζητά το brand ή δεν πρέπει να συνδέεται με αυτό εισάγεται εδώ.
Προαιρετικό CTA
CTA σημαίνει την ενέργεια που θέλετε να κάνει ο αναγνώστης μετά την ανάγνωση του άρθρου. Αν δεν είστε σίγουροι ποιο κείμενο είναι κατάλληλο, είναι καλύτερο να αφήσετε αυτό το πεδίο κενό.
Εσωτερική και εξωτερική διασύνδεση
Το OxiRanker μπορεί να εφαρμόζει έξυπνα εσωτερικούς και εξωτερικούς συνδέσμους μέσα στα άρθρα.
Εσωτερικοί σύνδεσμοι
Αυτή η επιλογή είναι ενεργή από προεπιλογή και είναι καλύτερο να παραμείνει ενεργή. Η εσωτερική διασύνδεση βοηθά τις μηχανές αναζήτησης να κατανοήσουν καλύτερα τη δομή περιεχομένου του ιστοτόπου σας.
Στο πρώτο άρθρο, συνήθως δεν δημιουργούνται εσωτερικοί σύνδεσμοι επειδή δεν υπάρχει ακόμη προηγούμενο άρθρο. Από το δεύτερο άρθρο και μετά, το σύστημα μπορεί να εκτελεί εσωτερική διασύνδεση.
Εξωτερικοί σύνδεσμοι
Οι εξωτερικοί σύνδεσμοι προστίθενται σε επίσημες και αξιόπιστες πηγές και επισημαίνονται ως nofollow ώστε να μην μεταφέρεται ακούσια SEO authority.
Έλεγχος ποιότητας προφίλ με AI
Μετά την ολοκλήρωση του Article Profile, το OxiRanker ελέγχει την ποιότητα του προφίλ.
Σύνδεση Telegram για περιλήψεις άρθρων
Μετά την επιτυχή αποθήκευση του προφίλ, μπορείτε να ενεργοποιήσετε την αποστολή περιλήψεων άρθρων σε ένα κανάλι Telegram.
Προσθήκη του bot
Προσθέστε το @Oxiranker_Bot στο κανάλι σας στο Telegram και κάντε το Admin.
Εισαγωγή ονόματος χρήστη καναλιού
Εισαγάγετε το όνομα του καναλιού με @. Παράδειγμα: @my_channel
Ενεργοποίηση αποστολής περίληψης
Μετά την καταχώριση ενός έγκυρου καναλιού, ενεργοποιήστε την αποστολή περίληψης άρθρων.
Ρύθμιση της αυτόματης μηχανής άρθρων
Στην καρτέλα Automatic article generation, μπορείτε να διαχειριστείτε τη μηχανή αυτόματης δημιουργίας άρθρων.
Έξυπνη επιλογή θέματος
Το σύστημα χρησιμοποιεί λέξεις-κλειδιά και Article Profile για να επιλέγει κατάλληλα, μοναδικά και δημοσιεύσιμα θέματα.
Δημιουργία με επίγνωση του brand
Ο τόνος, το κοινό, ο στόχος περιεχομένου και οι περιορισμοί του brand λαμβάνονται υπόψη κατά τη δημιουργία του άρθρου.
Προγραμματισμένη δημοσίευση
Η μηνιαία χωρητικότητα κατανέμεται μέσα στον μήνα ώστε η δημοσίευση περιεχομένου να φαίνεται πιο φυσική και συνεπής.
Μηνιαία χωρητικότητα
7 articles / month
10 articles / month
15 articles / month
30 articles / month30 άρθρα σημαίνει σχεδόν ένα άρθρο την ημέρα. 15 άρθρα σημαίνει σχεδόν μέρα παρά μέρα. 10 άρθρα σημαίνει περίπου κάθε δύο έως τρεις ημέρες. 7 άρθρα σημαίνει περίπου κάθε τρεις έως τέσσερις ημέρες.
Μήκος άρθρου και εικόνα
Το μήκος άρθρου μπορεί να είναι Short, Standard ή Long. Το Standard είναι μια ισορροπημένη επιλογή για τους περισσότερους ιστοτόπους.
Οι εικόνες άρθρων μπορούν να ενεργοποιηθούν ή να απενεργοποιηθούν και το θέμα της εικόνας μπορεί να είναι Light ή Dark.
Δημιουργία του πρώτου άρθρου από το Generate
Μετά την προετοιμασία του domain, ανοίξτε το μενού Generate, επιλέξτε το domain και κάντε κλικ στο New Content ή New Article.
Γλώσσα
Επιλέγεται μόνο μία γλώσσα για κάθε αίτημα περιεχομένου.
Θέμα
Γράψτε ένα σαφές θέμα. Το Source URL είναι προαιρετικό.
Εικόνα
Ενεργοποιήστε ή απενεργοποιήστε την εικόνα και επιλέξτε θέμα Light ή Dark.
Πληρωμή και δημιουργία
Το κόστος αφαιρείται από το wallet και ξεκινά η δημιουργία του άρθρου.
Έλεγχος Preview, SEO Report και JSON Output
Μετά τη δημιουργία του άρθρου, διατίθενται αρκετές σημαντικές έξοδοι για έλεγχο ποιότητας και προετοιμασία δημοσίευσης.
Preview
Εμφανίζει το κείμενο του άρθρου και τη δημιουργημένη εικόνα. Αυτή η ενότητα είναι χρήσιμη για τον έλεγχο της τελικής ποιότητας πριν από τη δημοσίευση.
SEO Report
Εμφανίζει μια κατανοητή αναφορά SEO που περιλαμβάνει SEO Score, title, meta description, canonical, keywords, schema, headings, links, images και OpenGraph.
JSON Output
Εμφανίζει την πλήρη έξοδο του άρθρου για τεχνικούς χρήστες, συμπεριλαμβανομένων των slug, html_body, schema, URL εικόνων, tags, category, reading time και timestamps δημιουργίας/ενημέρωσης.
Τι ακριβώς κάνουν το Export API και το Webhook;
Αφού δημιουργηθεί ένα άρθρο στο OxiRanker, ο ιστότοπος προορισμού μπορεί να το διαβάσει με Export API ή να ειδοποιηθεί μέσω Webhook όταν το άρθρο είναι έτοιμο.
Μέθοδος 1: Export API
Ο ιστότοπος προορισμού μπορεί να διαβάζει άρθρα από το OxiRanker με το Export Token όποτε χρειάζεται. Αυτή η μέθοδος χρησιμοποιείται για χειροκίνητο sync, ημερήσιο sync, σελίδες λίστας άρθρων και λήψη του πλήρους 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/jsonΜέθοδος 2: Webhook
Όταν δημιουργείται ένα νέο άρθρο ή ενημερώνεται ένα άρθρο, το OxiRanker ειδοποιεί το endpoint του ιστοτόπου προορισμού. Το Webhook δεν στέλνει το πλήρες άρθρο. Στέλνει μόνο το αναγνωριστικό του άρθρου ώστε ο ιστότοπος προορισμού να μπορεί να λάβει το πλήρες άρθρο από το 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"
}Ευθύνη αποθήκευσης περιεχομένου και Image Mirroring
Ο ιστότοπος προορισμού πρέπει να αποθηκεύει το τελικό άρθρο και τις εικόνες στη δική του υποδομή.
Τι πρέπει να αποθηκεύει ο ιστότοπος προορισμού
Παράδειγμα σωστής διαδρομής εικόνας
Η άμεση και μόνιμη χρήση URL εικόνων του OxiRanker δεν είναι σωστή. Η εικόνα πρέπει να ληφθεί και τα URL του άρθρου πρέπει να αντικατασταθούν με εσωτερικά URL του ιστοτόπου προορισμού.
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpΛήψη Export Token και έτοιμων endpoints
Το Export Token και τα έτοιμα προς χρήση endpoints είναι διαθέσιμα από τη σελίδα άρθρου, την καρτέλα Outputs και την ενότητα API Output.
Πού θα λάβετε το Export Token
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}Έτοιμα προς χρήση endpoints
Στην ίδια σελίδα άρθρου, μέσα στην ενότητα API Output, το Ready-to-use endpoints εμφανίζει προετοιμασμένα URL.
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 και endpoint προορισμού
Το Webhook Secret δημιουργείται επίσης από τη σελίδα άρθρου, την καρτέλα Outputs και την ενότητα API Output.
Πού δημιουργείται το Webhook Secret;
Σταθερό endpoint προορισμού
Το OxiRanker αποδέχεται μόνο την ακόλουθη σταθερή διαδρομή για το Webhook προορισμού. Ο ιστότοπος προορισμού πρέπει να δημιουργήσει αυτό το endpoint στο backend του.
POST /api/oxiranker/webhook
https://example.com/api/oxiranker/webhookΑυτό το endpoint πρέπει να αποδέχεται μόνο POST, να διατηρεί το raw body, να επαληθεύει timestamp και signature και να επιστρέφει επιτυχή απόκριση για webhook.test.
Απαιτούμενες τιμές .env για τον ιστότοπο προορισμού
Το OxiRanker δεν έχει πρόσβαση στα αρχεία του ιστοτόπου προορισμού ή στο .env. Ο προγραμματιστής του ιστοτόπου προορισμού πρέπει να αποθηκεύσει αυτές τις τιμές στο δικό του 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Τιμές ασφαλείας
Τιμές αποθήκευσης
Export API: List, JSON, HTML και ZIP
Ο ιστότοπος προορισμού μπορεί να χρησιμοποιήσει το Export API για να λάβει τη λίστα άρθρων ή την πλήρη έξοδο κάθε άρθρου.
Λήψη της λίστας άρθρων για ένα 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 ορίζει τον αριθμό των άρθρων και προς το παρόν περιορίζεται μεταξύ 1 και 50. Το offset χρησιμοποιείται για pagination. Το lang είναι προαιρετικό και χρησιμοποιείται για λήψη άρθρων σε συγκεκριμένη γλώσσα όπως fa, en ή tr.
Λήψη του πλήρους άρθρου ως JSON
GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonΑυτή είναι η πιο σημαντική έξοδος για τον ιστότοπο προορισμού. Ο ιστότοπος προορισμού πρέπει να αποθηκεύει το πλήρες JSON του άρθρου στη δική του βάση δεδομένων και να διαβάζει από τη δική του βάση δεδομένων για δημόσια εμφάνιση.
Λήψη έτοιμου 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 είναι κατάλληλο για ιστοτόπους που θέλουν έτοιμη έξοδο, αλλά η πιο επαγγελματική προσέγγιση είναι η αποθήκευση JSON και η απόδοσή του με το δικό template του ιστοτόπου προορισμού.
Λήψη εξόδου ZIP
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/zip
Authorization: Bearer YOUR_EXPORT_TOKENΗ έξοδος ZIP περιλαμβάνει article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt και README.txt.
Υπογραφή Webhook και ασφαλής λήψη Webhook
Ο ιστότοπος προορισμού πρέπει να επαληθεύει την υπογραφή Webhook χρησιμοποιώντας το αρχικό 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=...Τύπος υπογραφής
payload_to_sign = timestamp + "." + rawBody
signature = "sha256=" + HMAC_SHA256(webhook_secret, payload_to_sign)Αν ο ιστότοπος προορισμού κάνει πρώτα parse το JSON και έπειτα το ξανακάνει stringify, η σειρά ή τα κενά μπορεί να αλλάξουν και η υπογραφή να γίνει άκυρη. Το αρχικό raw body πρέπει να χρησιμοποιείται για την επαλήθευση της υπογραφής.
Απλό δείγμα κώδικα Node.js για λήψη Webhook
Αυτό το δείγμα Express χειρίζεται 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,
});
});Προτεινόμενη βάση δεδομένων ιστοτόπου προορισμού
Ο ιστότοπος προορισμού πρέπει να αποθηκεύει άρθρα στη δική του βάση δεδομένων και να αποτρέπει διπλότυπα με 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);Αποτροπή διπλοτύπων με UPSERT
Το άρθρο δεν πρέπει να εισάγεται ξανά για κάθε Webhook. Αν υπάρχει ήδη, πρέπει να ενημερώνεται.
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 του πλήρους άρθρου και sync μετά το Webhook
Όταν φτάσει ένα Webhook, ο ιστότοπος προορισμού πρέπει να κάνει fetch το πλήρες άρθρο από το Export API, να κάνει mirror τις εικόνες και να κάνει UPSERT το άρθρο.
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 και κανόνες αποθήκευσης εικόνων
Ο ιστότοπος προορισμού πρέπει να κατεβάζει εικόνες, να τις αποθηκεύει στη δική του υποδομή και να αντικαθιστά τα URL του άρθρου.
Κανόνες Image Mirroring
Προτεινόμενη διαδρομή αποθήκευσης
/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.webpΗμερήσιο sync για αξιοπιστία
Το Webhook από μόνο του δεν είναι αρκετό. Ο ιστότοπος προορισμού πρέπει επίσης να έχει ημερήσιο sync για την ανάκτηση χαμένων άρθρων.
Γιατί χρειάζεται ημερήσιο sync;
Αλγόριθμος ημερήσιου sync
Εμφάνιση άρθρων στον ιστότοπο προορισμού
Μετά την αποθήκευση άρθρων στο site_articles, ο ιστότοπος προορισμού δεν πρέπει να συνδέεται με το OxiRanker για κάθε δημόσια εμφάνιση. Πρέπει να διαβάζει από τη δική του βάση δεδομένων.
Σελίδα λίστας blog
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;Σελίδα λεπτομερειών άρθρου
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
Ο ιστότοπος προορισμού πρέπει να διαβάζει δεδομένα SEO από τη δική του βάση δεδομένων και να τα αποδίδει στη σελίδα άρθρου.
Πεδία SEO
HTML rendering
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.html_body }}
/>Αν ο ιστότοπος προορισμού έχει αυστηρή πολιτική ασφαλείας, μπορεί να κάνει sanitize το HTML πριν την αποθήκευση ή πριν την εμφάνιση.
RTL και LTR
Κάθε άρθρο έχει πεδία lang, dir και is_rtl. Αν το dir είναι rtl, η σελίδα άρθρου πρέπει να αποδίδεται από δεξιά προς τα αριστερά.
<article dir="rtl">
...
</article>Συνηθισμένα σφάλματα και η σημασία τους
Τα σφάλματα του ιστοτόπου προορισμού πρέπει να είναι στα αγγλικά, σαφή και κατανοητά ώστε η αντιμετώπιση προβλημάτων να γίνεται γρήγορα.
Σφάλματα Export API
Σφάλματα Webhook
Τελική λίστα ελέγχου API και Webhook
Μετά την υλοποίηση, αυτές οι δοκιμές επαληθεύουν τη σύνδεση από την αρχή μέχρι το τέλος.
Δοκιμή Export Token και JSON
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/jsonΔοκιμή Webhook
Αν καλέσετε το Webhook προορισμού με Postman χωρίς υπογραφή, πρέπει να λάβετε σφάλμα ασφαλείας. Αυτό σημαίνει ότι το endpoint είναι ενεργό και ελέγχει την ασφάλεια.
{
"error": "missing_webhook_signature",
"message": "Webhook signature header is required."
}Έπειτα κάντε κλικ στο Test Webhook μέσα στο OxiRanker. Αν ο receiver υποστηρίζει webhook.test, το response_status πρέπει να είναι 200.
Σημαντικοί κανόνες για σωστή υλοποίηση
Αυτοί οι κανόνες πρέπει να ακολουθούνται στην ενσωμάτωση API ώστε το σύστημα να παραμένει ασφαλές, σταθερό και συντηρήσιμο.
Πλήρης σύνοψη ροής API
Μετά την ολοκλήρωση αυτών των βημάτων, ο προσαρμοσμένος ιστότοπός σας μπορεί να λαμβάνει, να αποθηκεύει, να κάνει mirror και να δημοσιεύει με ασφάλεια άρθρα που δημιουργούνται από το OxiRanker.