OXIRANKER DEVELOPER DOCS

OxiRanker փաստաթղթեր

Մաքուր փաստաթղթերի կենտրոն՝ ձեր կայքը OxiRanker ծառայություններին միացնելու համար։ Սկսեք WordPress ինտեգրումից կամ API ինտեգրումից, ապա քայլ առ քայլ հետևեք ուղեցույցին։

Պաշտոնական փաստաթղթերFull-feature V1Կայքերի սեփականատերերի, ծրագրավորողների և ինտեգրման թիմերի համար
Փաստաթղթերի մենյու
Մենյուից ընտրեք ուղեցույց։ Այս էջը OxiRanker-ի հիմնական փաստաթղթերի կենտրոնն է։
API ծառայություն

API ծառայություն

Օգտագործեք Export API և Webhook անհատական կայքերի, անհատական CMS հարթակների, Next.js, Laravel, Node.js, PHP կամ Python backend-ների համար։

Domain
Հաստատված
Բովանդակություն
Պատրաստ
API
Webhook
Կարևոր պահման ծանուցում

OxiRanker-ը նպատակակետ կայքի համար մշտական hosting չէ

OxiRanker-ը նպատակակետ կայքի բովանդակության, պատկերների կամ արտահանված ֆայլերի համար մշտական hosting-ի երաշխիք չի տրամադրում։ OxiRanker-ը user-content hosting ծառայություն չէ, և ստեղծված բովանդակությունը, cover պատկերները, Open Graph պատկերները կամ output ֆայլերը կարող են OxiRanker ենթակառուցվածքում չմնալ 90 օրից ավելի։

Հետևաբար, output-ը ստանալուց հետո նպատակակետ կայքը պետք է վերջնական հոդվածը, պատկերները, SEO տվյալները, Open Graph տվյալները և Schema տվյալները պահի իր սեփական database-ում, server-ում, storage-ում կամ CDN-ում և չպետք է public display-ի համար կախված լինի OxiRanker URL-ներից։

Ուսուցողական տեսանյութ

Միացման ուսուցողական տեսանյութ

Օգտագործեք այս տեսանյութը որպես տեսողական ուղեցույց՝ նախքան WordPress-ը միացնելը կամ API ինտեգրումը իրականացնելը։

API Output

Իրականացրեք output flow-ը քայլ առ քայլ

Նախ պահեք Export Token-ը և Webhook Secret-ը ձեր backend-ում, ապա ավարտեք Webhook receiver-ը, database-ը, UPSERT logic-ը, Image Mirroring-ը և օրական sync-ը։

ՔԱՅԼ 01

Գրանցվել և ստեղծել domain OxiRanker-ում

Նախ գրանցվեք OxiRanker-ում, մուտք գործեք ձեր account և Domains բաժնից սեղմեք Add Domain։

Domain-ի ճիշտ ձևաչափ

Domain name դաշտում մուտքագրեք միայն root domain-ը։ Հասցեն չպետք է ներառի http, https կամ www։

example.com

Սխալ ձևաչափեր

Այս ձևաչափերը մի մուտքագրեք domain դաշտում։

https://example.com
http://example.com
www.example.com
Domain ստեղծելուց հետո
Եթե domain ձևաչափը valid է, OxiRanker-ը ստեղծում է domain-ը և ձեզ տեղափոխում է domain ownership verification քայլին։
ՔԱՅԼ 02

Հաստատել domain-ի սեփականությունը

Այս քայլում դուք պետք է ապացուցեք, որ domain-ը պատկանում է ձեզ կամ ունեք այն կառավարելու հասանելիություն։

DNS TXT

Առաջարկվում է

Ավելացրեք TXT record domain DNS-ում։ Cloudflare-ի համար սա սովորաբար ամենապարզ մեթոդն է։

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

Տեղադրեք text file ձեր կայքի .well-known path-ում, որպեսզի OxiRanker-ը կարողանա այն verify անել։

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

File Content:
220daaa37****42730f27

Meta Tag

Տեղադրեք meta tag ձեր կայքի homepage-ի head section-ում։

<meta name="yourdomain-verification" content="190208d****d275b65" />
Կարևոր DNS նշում
DNS record ավելացնելուց հետո, հատկապես Cloudflare-ում, հնարավոր է պետք լինի սպասել 2-ից 3 րոպե։ Այնուհետև նույն OxiRanker էջում սեղմեք Confirm։
Verification token-ի վավերականության ժամկետ
Domain verification values-ը ունեն ժամանակային սահմանափակում։ Ներկայումս verification values-ը սովորաբար valid են 1440 րոպե։
ՔԱՅԼ 03

Ստանալ անվճար token-ներ առաջին domain-ը verify անելուց հետո

Առաջին domain-ը verify անելուց հետո OxiRanker-ը ձեր account-ին ավելացնում է 1000 անվճար token initial testing-ի համար։

Միայն առաջին domain-ի համար
Այս նվերը ավելացվում է միայն առաջին verified domain-ի համար և հետագա domains-ի համար կրկին չի ավելացվում։
ՔԱՅԼ 04

Կարգավորել լեզուները և Blog URL-ը

Blog URLs բաժնում սահմանեք՝ ձեր կայքը մեկ լեզվո՞վ է, թե՞ բազմալեզու, և որ լեզուները պետք է օգտագործվեն 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-ը, որտեղ articles-ը պետք է publish լինեն։
ՔԱՅԼ 05

Ավելացնել keywords յուրաքանչյուր լեզվի համար

Keywords tab-ում յուրաքանչյուր լեզվի համար ավելացրեք առնվազն 3 և առավելագույնը 50 keywords։

Հիմնական կանոն

Յուրաքանչյուր լեզվի keywords-ը պետք է մուտքագրվեն նույն լեզվով։ Եթե article language-ը English է, keywords-ը նույնպես պետք է English լինեն։

Ինչպես ավելացնել keywords

Յուրաքանչյուր keyword մուտքագրելուց հետո սեղմեք Enter՝ այն ավելացնելու համար։ Բոլոր keywords-ը լրացնելուց հետո սեղմեք Save։

Հաճախ հանդիպող սխալ
Եթե English language-ի համար Turkish keywords մուտքագրեք, English content-ում կարող են հայտնվել Turkish phrases։ Keyword language-ը պետք է համապատասխանի article language-ին։
ՔԱՅԼ 06

Լրացնել domain Article Profile-ը

Article Profile-ը OxiRanker-ին ասում է՝ ինչ է ձեր brand-ը, ում համար է գրում, ինչ tone պետք է օգտագործի և որ topics-ից պետք է խուսափի։

Industry

Սահմանում է market-ը կամ business category-ն։ Օրինակ՝ real estate consulting և property buying and selling Dubai-ում։

Business description

Բացատրում է՝ business-ը ինչ է վաճառում, ում է ծառայում, ինչպես է աշխատում և ինչն է այն տարբեր դարձնում։

Audience

Սահմանում է՝ content-ը ում հետ պետք է խոսի։ Յուրաքանչյուր audience կարող է պահվել առանձին։

Tone

Կառավարում է generated content-ի style-ը և voice-ը, օրինակ՝ formal, educational, simple, technical կամ sales-oriented։

Content goal

Սահմանում է՝ articles-ը ինչ պետք է հասնեն, օրինակ՝ customers attract անել, traffic ավելացնել կամ users-ին educate անել։

Արգելված թեմաներ

Այն ամենը, ինչի մասին brand-ը չպետք է խոսի կամ ինչի հետ չպետք է ասոցացվի, մուտքագրվում է այստեղ։

Optional CTA

CTA նշանակում է այն գործողությունը, որը ցանկանում եք, որ reader-ը կատարի article-ը կարդալուց հետո։ Եթե վստահ չեք, թե ինչ text է suitable, ավելի լավ է այս field-ը թողնել empty։

Multilingual profile
Եթե ձեր կայքը multilingual է, պետք է profile-ը լրացնել միայն մեկ լեզվով։ OxiRanker-ը նույն profile-ի հիման վրա կարող է պատրաստել անհրաժեշտ versions-ը այլ լեզուների համար։
ՔԱՅԼ 07

Ներքին և արտաքին հղումներ

OxiRanker-ը կարող է articles-ի ներսում խելացիորեն կիրառել internal և external links։

Internal links

Այս option-ը default enabled է, և ավելի լավ է այն պահել enabled։ Internal linking-ը search engines-ին օգնում է ավելի լավ հասկանալ ձեր կայքի content structure-ը։

Առաջին article-ում internal links սովորաբար չեն ստեղծվում, քանի որ դեռ previous article չկա։ Երկրորդ article-ից սկսած՝ system-ը կարող է perform անել internal linking։

External links

External links-ը ավելացվում են official և trusted sources-ին և նշվում են որպես nofollow, որպեսզի SEO authority-ն unintentionally չփոխանցվի։

ՔԱՅԼ 08

Ստուգել profile quality-ն AI-ով

Article Profile-ը լրացնելուց հետո OxiRanker-ը review է անում profile quality-ն։

85+
Save անելու համար պահանջվող minimum score
95
Improvements-ից հետո սովորաբար հասանելի excellent score
AI
Professional suggestions յուրաքանչյուր field-ի համար
Եթե score-ը 85-ից ցածր է
System-ը final saving թույլ չի տալիս և յուրաքանչյուր problematic field-ի համար ցուցադրում է explanations և improvement suggestions։ Կարող եք կիրառել suggestions-ը և կրկին save անել։
ՔԱՅԼ 09

Միացնել Telegram-ը article summaries-ի համար

Profile-ը հաջողությամբ save անելուց հետո կարող եք 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-ն image-ի և link-ի հետ ուղարկվում է Telegram channel։ Սա կարող է ուժեղացնել traffic paths-ը դեպի ձեր կայք։
ՔԱՅԼ 10

Կարգավորել automatic article engine-ը

Automatic article generation tab-ում կարող եք manage անել automatic article generation engine-ը։

Smart topic selection

System-ը օգտագործում է keywords և Article Profile՝ suitable, unique և publishable topics ընտրելու համար։

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 կայքերի համար
Automatic engine-ը articles-ը ստեղծում է OxiRanker-ի ներսում։ Generation-ից հետո յուրաքանչյուր article-ը ձեր custom website տեղափոխելու համար պետք է implement անել Export API, Webhook, database storage և Image Mirroring։
ՔԱՅԼ 11

Ստեղծել առաջին article-ը Generate-ից

Domain-ը պատրաստելուց հետո բացեք Generate menu-ն, ընտրեք domain-ը և սեղմեք New Content կամ New Article։

1

Language

Յուրաքանչյուր content request-ի համար ընտրվում է միայն մեկ language։

2

Topic

Գրեք հստակ topic։ Source URL-ը optional է։

3

Image

Enable կամ disable արեք image-ը և ընտրեք Light կամ Dark theme։

4

Payment և generation

Cost-ը deducted է լինում wallet-ից և article generation-ը սկսվում է։

Generation time
Article և image generation-ը սովորաբար տևում է մոտ 2-ից 3 րոպե։ Ավարտից հետո article-ը տեսանելի է OxiRanker-ի ներսում։
Source URL
Եթե Source URL մուտքագրված է, OxiRanker-ը այն օգտագործում է topic-ը ավելի լավ հասկանալու համար, բայց copied content չի generate անում։ System-ը designed է unique content ստեղծելու համար։
ՔԱՅԼ 12

Review անել Preview, SEO Report և JSON Output

Article-ը generate անելուց հետո quality review-ի և publishing preparation-ի համար հասանելի են մի քանի կարևոր outputs։

Preview

Ցուցադրում է article text-ը և generated image-ը։ Այս բաժինը useful է final quality-ն publishing-ից առաջ review անելու համար։

SEO Report

Ցուցադրում է հասկանալի SEO report՝ ներառյալ 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-ը

OxiRanker-ում article generate լինելուց հետո նպատակակետ կայքը կարող է այն կարդալ Export API-ով կամ ստանալ ծանուցում Webhook-ով, երբ article-ը պատրաստ է։

Մեթոդ 1: Export API

Նպատակակետ կայքը կարող է անհրաժեշտության դեպքում Export Token-ով կարդալ articles-ը OxiRanker-ից։ Այս method-ը օգտագործվում է manual sync-ի, daily sync-ի, article list pages-ի և full article JSON fetch անելու համար։

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

Մեթոդ 2: Webhook

Երբ new article է ստեղծվում կամ 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-ը չպետք է ակնկալի full article Webhook-ի ներսում։ domain_id և article_id ստանալուց հետո այն պետք է fetch անի complete article-ը Export JSON API-ից և store անի իր database-ում։
ՔԱՅԼ 14

Content storage responsibility և Image Mirroring

Destination website-ը պետք է final article-ը և images-ը store անի իր սեփական infrastructure-ում։

Ինչ պետք է store անի destination website-ը

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 original output-ը պահպանելու համար

Correct image path example

OxiRanker image URLs-ը directly և permanently օգտագործելը ճիշտ չէ։ Image-ը պետք է download արվի, և article URLs-ը պետք է replace արվեն destination website-ի internal URLs-ով։

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker-ը destination website-ի համար permanent hosting չէ
OxiRanker-ը պատասխանատու չէ destination website-ի output content-ը և images-ը permanently store անելու համար։ OxiRanker-ը user-content hosting service չէ, և generated content-ը, cover images-ը, Open Graph images-ը կամ output files-ը կարող են OxiRanker infrastructure-ում 90 օրից ավելի չմնալ։ Հետևաբար, destination website-ը պետք է final article-ը, images-ը, SEO data-ն, OpenGraph-ը և Schema-ն store անի իր database-ում, server-ում, storage-ում կամ CDN-ում։
ՔԱՅԼ 15

Ստանալ Export Token և ready endpoints

Export Token-ը և ready-to-use endpoints-ը հասանելի են article page-ից, Outputs tab-ից և API Output section-ից։

Որտեղից ստանալ Export Token-ը

Ստեղծեք առաջին article-ը OxiRanker-ում։
Բացեք այդ article page-ը։
Բացեք Outputs tab-ը։
Բացեք API Output section-ը։
Ստեղծեք token Token management-ից։
Full token-ը ցուցադրվում է միայն մեկ անգամ։
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
Token-ի rotation
Եթե Export Token-ը rotated է լինում OxiRanker panel-ում, previous token-ը expires է լինում։ New value-ն պետք է անմիջապես replace անի old value-ն destination website .env-ում կամ backend environment-ում, և destination backend-ը պետք է restart կամ redeploy արվի։
ՔԱՅԼ 16

Ստեղծել Webhook Secret և destination endpoint

Webhook Secret-ը նույնպես ստեղծվում է article page-ից, Outputs tab-ից և API Output section-ից։

Որտե՞ղ է ստեղծվում Webhook Secret-ը

Բացեք article page-ը։
Բացեք Outputs tab-ը։
Բացեք API Output section-ը։
Ստեղծեք secret Webhook Secret section-ում։
Full secret-ը ցուցադրվում է միայն մեկ անգամ։
Store արեք secret-ը destination website .env-ում։

Fixed destination endpoint

OxiRanker-ը destination Webhook-ի համար ընդունում է միայն հետևյալ fixed path-ը։ Destination website-ը պետք է այս endpoint-ը ստեղծի իր backend-ում։

POST /api/oxiranker/webhook

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

Այս endpoint-ը պետք է ընդունի միայն POST, preserve անի raw body-ն, verify անի timestamp-ը և signature-ը, և վերադարձնի successful response webhook.test-ի համար։

Active events
Current version-ում events-ը fixed են՝ article.created և article.updated։ Connection-ը testing անելու համար ուղարկվում է webhook.test event value։
ՔԱՅԼ 17

Destination website-ի required .env values

OxiRanker-ը չունի access destination website files-ին կամ .env-ին։ Destination website developer-ը պետք է այս values-ը store անի իր 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-ը պետք է store լինի միայն backend-ում։
OXIRANKER_WEBHOOK_SECRET
Webhook Secret-ը պետք է store լինի միայն backend-ում։
OXIRANKER_WEBHOOK_MAX_SKEW_MS
Webhook-ի համար maximum allowed time difference։ 300000 նշանակում է 5 րոպե։

Storage values

SITE_ARTICLE_IMAGE_UPLOAD_DIR
Destination server-ում mirrored images store անելու physical path-ը։
SITE_ARTICLE_IMAGE_MAX_BYTES
Download-ի և storage-ի համար maximum allowed image size-ը։
PUBLIC_BASE_URL
Final image և article URLs ստեղծելու համար օգտագործվող public destination website URL-ը։
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 article list-ը կամ յուրաքանչյուր article-ի full output-ը ստանալու համար։

Ստանալ articles list-ը 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-ը սահմանում է articles-ի քանակը և ներկայումս սահմանափակված է 1-ից 50-ի միջև։ offset-ը օգտագործվում է pagination-ի համար։ lang-ը optional է և օգտագործվում է specific language-ի articles-ը fetch անելու համար, օրինակ՝ 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

Սա destination website-ի համար ամենակարևոր output-ն է։ Destination website-ը պետք է full article JSON-ը store անի իր սեփական database-ում և public display-ի համար read անի իր սեփական database-ից։

Ստանալ 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-ը suitable է այն websites-ի համար, որոնք ցանկանում են ready output, բայց ավելի professional approach-ը JSON store անելն է և այն destination website-ի սեփական template-ով render անելը։

Ստանալ 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։ Signature verification-ի համար պետք է օգտագործել original raw body-ն։

Timing-safe comparison
Signatures-ը մի համեմատեք expected === received-ով։ Node.js-ում ավելի լավ է օգտագործել crypto.timingSafeEqual։
ՔԱՅԼ 20

Պարզ Node.js code sample 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-ում Persian կամ Unicode text-ի համար օգտագործվում են simple quotes։ N'...' նման բան պետք չէ և չպետք է օգտագործվի։
ՔԱՅԼ 22

Prevent duplicates UPSERT-ով

Article-ը չպետք է կրկին inserted լինի յուրաքանչյուր Webhook-ի համար։ Եթե այն արդեն exists է, այն պետք է updated լինի։

Main key duplicates կանխելու համար
Duplicates կանխելու ամենակարևոր constraint-ը սա է՝ 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-ը պետք է Export API-ից fetch անի full article-ը, 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 անի իր own infrastructure-ում և replace անի article URLs-ը։

Image Mirroring rules

Միայն HTTPS URLs-ը պետք է accepted լինեն։
File-ը պետք է իրականում image լինի։
content-type-ը պետք է սկսվի image/-ով։
Empty files-ը չպետք է store լինեն։
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 missed articles-ը recover անելու համար։

Ինչու՞ է daily sync-ը պետք

Destination website-ը կարող է ժամանակավորապես down լինել։
Webhook-ը կարող է timeout լինել։
Կարող են լինել deployment կամ network issues։
Article-ը կարող է generated լինել, բայց դրա Webhook-ը missed լինել։

Daily sync algorithm

Սկսեք offset-ը 0-ից։
Օգտագործեք limit 20 կամ 50։
Fetch արեք article list-ը Export List API-ից։
Յուրաքանչյուր item-ի համար fetch արեք այդ article-ի Export JSON-ը։
Mirror արեք images-ը։
Store արեք article-ը UPSERT-ով։
Increase արեք offset-ը և continue արեք մինչև total-ը completed լինի։
ՔԱՅԼ 26

Ցուցադրել articles-ը destination website-ում

Articles-ը site_articles-ում store անելուց հետո destination website-ը չպետք է յուրաքանչյուր public display-ի համար connect լինի OxiRanker-ին։ Այն պետք է 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-ի համար կարող եք օգտագործել /{lang}/blog/{slug} pattern-ը։ Օրինակ՝ /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-ը save-ից առաջ կամ display-ից առաջ։

RTL և LTR

Յուրաքանչյուր article ունի lang, dir և is_rtl fields։ Եթե dir-ը rtl է, article page-ը պետք է render լինի right-to-left։

<article dir="rtl">
  ...
</article>
ՔԱՅԼ 28

Հաճախ հանդիպող 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-ը սխալ է, rotated է, այս domain-ի հետ կապված չէ կամ domainId-ը սխալ է։
Token-ը չունի required scope։
Token-ը չունի required scope։ Նոր tokens-ը պետք է ունենան articles:read և exports:read։
lang-ը invalid է։
Query parameter-ում lang value-ն սխալ է ուղարկվել։

Webhook errors

Webhook URL-ը պետք է օգտագործի HTTPS։
Webhook URL-ը պետք է սկսվի https://-ով։
Webhook signature-ը invalid է։
Webhook Secret-ը սխալ է, secret-ը rotated է, raw body-ն փոխվել է կամ signature-ը սխալ է հաշվարկվել։
Webhook timestamp-ը expired կամ invalid է։
Timestamp-ը հին է, ապագայում է կամ invalid է։ Ստուգեք server clock-ը և NTP-ն։
Webhook test delivery-ը ձախողվեց։
Destination website-ը successful 200-ից 299 status չվերադարձրեց, timed out եղավ կամ webhook.test-ը ճիշտ չhandle արեց։
ՔԱՅԼ 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

Եթե destination Webhook-ը Postman-ով call եք անում առանց signature-ի, պետք է ստանաք 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-ը պետք է follow արվեն API integration-ում, որպեսզի system-ը մնա secure, stable և maintainable։

Export Token-ը պետք է store լինի միայն backend-ում։
Webhook Secret-ը պետք է store լինի միայն backend-ում։
Destination Webhook endpoint-ը պետք է լինի հենց /api/oxiranker/webhook։
Webhook URL-ը պետք է օգտագործի HTTPS։
Webhook signature-ը և timestamp-ը միշտ պետք է verified լինեն։
Articles-ը պետք է store լինեն UPSERT-ով, ոչ թե simple insert-ով։
Duplicates կանխելու համար required է unique constraint։
Images-ը պետք է download արվեն OxiRanker-ից և store արվեն destination website-ում։
Destination website-ը չպետք է permanently depend անի OxiRanker image URLs-ի վրա։
Image URLs-ը պետք է replace արվեն html_body-ի և article_schema-ի ներսում։
Եթե Webhook-ը fail է լինում, daily sync-ը պետք է recover անի missed article-ը։
Users-ին կամ logs-ում ցուցադրվող errors-ը պետք է լինեն English և understandable։
Եթե article-ը archived է, automatic sync-ը չպետք է այն կրկին publish անի։
raw_detail_payload և raw_list_item պետք է preserved լինեն, որպեսզի original output data չկորչի։
Եթե Export Token-ը կամ Webhook Secret-ը rotated է, new value-ն պետք է replace արվի .env-ում, և backend-ը պետք է restart կամ redeploy արվի։
ՔԱՅԼ 31

Complete API flow summary

Այս steps-ը ավարտելուց հետո ձեր custom website-ը կարող է securely receive, store, mirror և publish անել OxiRanker-ում generated articles-ը։

Գրանցվել OxiRanker-ում
Ավելացնել domain
Հաստատել domain ownership-ը
Ընտրել languages և ավելացնել Blog URL
Ավելացնել keywords յուրաքանչյուր language-ի համար
Լրացնել Article Profile
Կարգավորել Telegram-ը, եթե պետք է
Կարգավորել automatic article engine-ը
Ստեղծել առաջին article-ը Generate-ից
Review անել Preview և SEO Report
Ստանալ Export Token API Output-ից
Ստանալ ready Export API endpoints
Ստանալ Webhook Secret
Ստեղծել fixed /api/oxiranker/webhook endpoint-ը destination backend-ում
Store անել Token և Secret destination website .env-ում
Ստեղծել 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