Nasıl çalışır
Webhook push modelidir: Seomio içeriği ürettiği anda sizin endpoint'inize bir POST isteği gönderir. Siz bir şey sormazsınız, içerik size gelir. Sitenizin yapması gereken tek şey bu isteği karşılayacak tek bir adres açmaktır.
Seomio üretir
İçerik planına göre yazı, kapak görseli, meta etiketler ve JSON-LD hazırlanır.
Endpoint'inize gönderir
Tek bir POST isteğiyle tam HTML gövde ve tüm SEO alanları size ulaşır.
Siz yayınlarsınız
Kaydı oluşturur, id'yi döndürürsünüz. Sonraki güncelleme ve silmeler bu id ile gelir.
Hangi yöntemi seçmeliyim?
Kurulum
Panelde üç alan doldurulur, sitenizde tek bir endpoint açılır.
- 1
Sitenizde bir endpoint açın
JSON gövde kabul eden birPOSTadresi yeterlidir. Örnek:https://siteniz.com/seomio-webhook. Aşağıdaki örnek alıcı kodları doğrudan kullanabilirsiniz. - 2
Kendi API anahtarınızı belirleyin
Tahmin edilemez uzun bir dize üretin (ör. 32+ karakter). Bu anahtarı endpoint'inizde doğrulayacaksınız — Seomio her istekteAuthorizationbaşlığında gönderir. - 3
Panelde platformu seçin
Panel → SEO → Ayarlar → Yayın Entegrasyonu bölümünde platform olarak Custom API / Webhook seçin; endpoint adresinizi ve API anahtarınızı girin. - 4
Kimlik doğrulama biçimini seçin
Varsayılan Bearer token'dır (Authorization: Bearer <key>). Sisteminiz token'ı öneksiz bekliyorsa Ham token seçeneğini kullanın, aksi halde401alırsınız. - 5
Test isteği gönderin
Aynı ekrandaki Test İsteği Gönder düğmesi, gerçek yayınla birebir aynı isteği yollar ve sonucu HTTP koduyla birlikte gösterir. Ayrıntı: Test etme.
İstek formatı ve kimlik doğrulama
Tüm istekler tek adrese, POST metoduyla ve application/json içerik tipiyle gider. Yayınlama, güncelleme ve silme ayrımı gövdedeki action alanındadır — ayrı URL veya ayrı metot tanımlamanız gerekmez.
POST /seomio-webhook HTTP/1.1
Host: siteniz.com
Content-Type: application/json
Authorization: Bearer SIZIN_API_KEYINIZ| Alan | Tip | Açıklama |
|---|---|---|
| action | string | create · update · delete. İşlemin ne olduğunu bu alan söyler. |
| externalId | string | null | Sizin sisteminizdeki kayıt id'si. create'te null gelir; update ve delete'te create yanıtında döndürdüğünüz id geri gönderilir. |
| Authorization | header | Panelde girdiğiniz API Key. Varsayılan biçim Bearer <key>, "Ham token" seçilirse anahtar öneksiz gönderilir. Boş bırakılırsa başlık hiç eklenmez. |
| Ek başlıklar | header | Entegrasyon ayarlarındaki özel başlıklar (varsa) her isteğe eklenir. |
Yönlendirme (redirect) kullanmayın
301/302 ile başka bir adrese yönlendiriyorsa Authorization başlığı yönlendirmede düşer ve istek 401 alır. Panele nihai adresi yazın (www / https farkına dikkat).Yayınlama — create ve update
Yeni yazı action: "create" ile gelir, externalId boştur. Aynı yazı sonradan güncellenirse action: "update" ve dolu bir externalId ile aynı adrese gönderilir.
https://siteniz.com/seomio-webhook{
"action": "create",
"externalId": null,
"title": "PVC Zemin Bakımı: Dayanıklılığı Nasıl Korursunuz?",
"slug": "pvc-zemin-bakimi",
"content": "<div class=\"key-takeaways\">...</div><h2>Giriş</h2><p>...</p>",
"excerpt": "PVC zeminlerin ömrünü uzatan bakım adımları.",
"keyword": "pvc zemin bakımı",
"metaTitle": "PVC Zemin Bakımı | Marka",
"metaDescription": "PVC zemin bakımı için pratik ve uzman önerileri.",
"coverImageUrl": "https://cdn.seomio.com.tr/blog-images/cover-8f21.png",
"publishedDate": "2026-08-19T09:00:00.000Z"
}| Alan | Tip | Açıklama |
|---|---|---|
| title | string | Yazı başlığı. |
| slug | string | URL için hazırlanmış, harf-rakam-tire biçiminde adres parçası. |
| content | string (HTML) | Tam HTML gövde. İçinde gövde görselleri (CDN üzerinden mutlak URL), "İlgili İçerikler" bölümü ve JSON-LD yapısal verisi (Article + FAQ + Breadcrumb) gömülü gelir. Olduğu gibi yayınlayın, ayrıştırmaya veya temizlemeye gerek yok. |
| excerpt | string | Kısa özet — liste ve kart görünümleri için. |
| keyword | string | Yazının hedef anahtar kelimesi. |
| metaTitle | string | Sayfanın <title> etiketi için önerilen değer. |
| metaDescription | string | Meta açıklama. |
| coverImageUrl | string (URL) | Kapak görselinin mutlak CDN adresi. Doğrudan <img src> olarak kullanabilir veya kendi medya kitaplığınıza indirebilirsiniz; erişim için ek kimlik doğrulama gerekmez. |
| publishedDate | string (ISO 8601) | Yayın tarihi. |
{
"action": "update",
"externalId": "1421",
"title": "PVC Zemin Bakımı: 2026 Güncel Rehber",
"slug": "pvc-zemin-bakimi",
"content": "<h2>Güncellenmiş içerik</h2><p>...</p>",
"excerpt": "Güncellenmiş özet.",
"keyword": "pvc zemin bakımı",
"metaTitle": "PVC Zemin Bakımı 2026 | Marka",
"metaDescription": "2026 için güncellenmiş bakım rehberi.",
"coverImageUrl": "https://cdn.seomio.com.tr/blog-images/cover-8f21.png",
"publishedDate": "2026-08-19T09:00:00.000Z"
}update için 404 dönebilirsiniz
externalId'ye karşılık bir kaydınız kalmadıysa 404 dönün. Seomio bunu görür ve aynı içeriği yeni kayıt olarak (create) yeniden gönderir — yayın kilitlenmez.Silme — delete
Bir yazı Seomio panelinden silindiğinde (veya arşivlenirken "siteden de kaldır" seçildiğinde) aynı endpoint'e action: "delete" gövdesiyle bir istek gönderilir. Böylece paneldeki silme işlemi sitenize de yansır; elle temizlik yapmanız gerekmez.
https://siteniz.com/seomio-webhook{
"action": "delete",
"externalId": "1421",
"slug": "pvc-zemin-bakimi",
"title": "PVC Zemin Bakımı: 2026 Güncel Rehber",
"deletedDate": "2026-08-19T11:20:00.000Z"
}| Alan | Tip | Açıklama |
|---|---|---|
| action | string | Her zaman "delete". |
| externalId | string | Silinecek kaydı adresleyen tek alan. Create yanıtında döndürdüğünüz id'dir. |
| slug | string | null | Yalnızca log ve eşleştirme kolaylığı için gönderilir. |
| title | string | null | Yalnızca log ve eşleştirme kolaylığı için gönderilir. |
| deletedDate | string (ISO 8601) | Silme isteğinin gönderildiği an. |
Yanıtınız nasıl yorumlanır
| HTTP | Seomio ne yapar |
|---|---|
| 2xx | Kaldırıldı sayılır; panelde "sitenizden kaldırıldı" bilgisi görünür. |
| 404 / 410 | Kayıt sizde zaten yok kabul edilir ve bu da başarı sayılır. |
| 405 / 501 | Endpoint'in silmeyi desteklemediği anlaşılır; panelde "silme desteği ekleyin" uyarısı çıkar. |
| Diğer hatalar | Yazı Seomio'da yine de silinir, panelde "sitenizden kaldırılamadı" notu görünür. İstek tekrarlanmaz. |
Silme isteği yayınla aynı adrese gider
DELETE metodu veya ayrı bir yol beklemeyin. Endpoint'iniz action alanını okumuyorsa, silme isteğini yeni yazı sanıp boş bir kayıt oluşturabilir. Alıcı kodunuzda delete dalını en başa koyun.Silme senkronunu kapatmak
syncDeletes değerini false yapın — bu durumda silme isteği hiç gönderilmez, yazı yalnızca Seomio panelinden kalkar.curl -X POST "https://siteniz.com/seomio-webhook" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer SIZIN_API_KEYINIZ" \
-d '{
"action": "delete",
"externalId": "1421",
"slug": "pvc-zemin-bakimi"
}'Sizden beklenen yanıt
2xx dönen her yanıt başarı sayılır. Kritik olan tek şey: create yanıtında kaydınızın id'sini döndürmeniz.
{
"id": "1421"
}{
"id": "1421",
"deleted": true
}id alanı neden bu kadar önemli?
externalId'si olarak saklanır ve sonraki güncelleme ve silme isteklerinde size geri gönderilir. Bu alanı döndürmezseniz her güncelleme yeni bir kayıt olarak gelir; sitenizde aynı yazının kopyaları birikir ve silme senkronu hiç çalışmaz.id, postId veya externalId adlarından herhangi biri kabul edilir.Örnek alıcı kodlar
Üç dalı da (create / update / delete) karşılayan, doğrudan uyarlanabilir örnekler.
const express = require("express");
const app = express();
// İçerik HTML gövdesiyle gelir — varsayılan 100kb limiti yetmeyebilir
app.use(express.json({ limit: "5mb" }));
const SEOMIO_KEY = process.env.SEOMIO_WEBHOOK_KEY;
app.post("/seomio-webhook", async (req, res) => {
// 1) Kimlik doğrulama — panelde girdiğiniz API Key
const token = (req.headers.authorization || "").replace(/^Bearer /i, "");
if (!SEOMIO_KEY || token !== SEOMIO_KEY) {
return res.status(401).json({ message: "unauthorized" });
}
const b = req.body || {};
// 2) SİLME — Seomio'da silinen yazı sitenizden de kalksın
if (b.action === "delete") {
const removed = await Post.deleteBySeomioRef(b.externalId);
if (!removed) return res.status(404).json({ message: "not found" });
return res.json({ id: b.externalId, deleted: true });
}
// 3) GÜNCELLEME — externalId sizin daha önce döndürdüğünüz id'dir
if (b.action === "update" && b.externalId) {
const updated = await Post.update(b.externalId, {
title: b.title,
slug: b.slug,
html: b.content,
excerpt: b.excerpt,
metaTitle: b.metaTitle,
metaDescription: b.metaDescription,
coverImageUrl: b.coverImageUrl
});
if (!updated) return res.status(404).json({ message: "not found" });
return res.json({ id: b.externalId });
}
// 4) YENİ YAZI — yanıtta id DÖNDÜRÜN, Seomio bunu saklar
const post = await Post.create({
title: b.title,
slug: b.slug,
html: b.content,
excerpt: b.excerpt,
metaTitle: b.metaTitle,
metaDescription: b.metaDescription,
coverImageUrl: b.coverImageUrl,
publishedAt: b.publishedDate
});
return res.json({ id: String(post.id) });
});
app.listen(3000);<?php
// routes/api.php — Route::post('/seomio-webhook', SeomioWebhookController::class);
class SeomioWebhookController extends Controller
{
public function __invoke(Request $request)
{
// 1) Kimlik doğrulama
$token = str_replace('Bearer ', '', $request->header('Authorization', ''));
if ($token !== config('services.seomio.webhook_key')) {
return response()->json(['message' => 'unauthorized'], 401);
}
$action = $request->input('action', 'create');
$externalId = $request->input('externalId');
// 2) SİLME
if ($action === 'delete') {
$post = Post::where('seomio_ref', $externalId)->first();
if (!$post) {
return response()->json(['message' => 'not found'], 404);
}
$post->delete();
return response()->json(['id' => $externalId, 'deleted' => true]);
}
$data = [
'title' => $request->input('title'),
'slug' => $request->input('slug'),
'body' => $request->input('content'),
'excerpt' => $request->input('excerpt'),
'meta_title' => $request->input('metaTitle'),
'meta_description' => $request->input('metaDescription'),
'cover_url' => $request->input('coverImageUrl'),
'published_at' => $request->input('publishedDate'),
];
// 3) GÜNCELLEME
if ($action === 'update' && $externalId) {
$post = Post::find($externalId);
if (!$post) {
return response()->json(['message' => 'not found'], 404);
}
$post->update($data);
return response()->json(['id' => (string) $post->id]);
}
// 4) YENİ YAZI
$post = Post::create($data);
return response()->json(['id' => (string) $post->id]);
}
}// app/api/seomio-webhook/route.ts (Next.js App Router)
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const token = (req.headers.get("authorization") || "").replace(/^Bearer /i, "");
if (token !== process.env.SEOMIO_WEBHOOK_KEY) {
return NextResponse.json({ message: "unauthorized" }, { status: 401 });
}
const body = await req.json();
if (body.action === "delete") {
const removed = await db.post.deleteMany({ where: { seomioRef: body.externalId } });
if (!removed.count) {
return NextResponse.json({ message: "not found" }, { status: 404 });
}
return NextResponse.json({ id: body.externalId, deleted: true });
}
if (body.action === "update" && body.externalId) {
const post = await db.post.update({
where: { id: body.externalId },
data: { title: body.title, slug: body.slug, html: body.content }
});
return NextResponse.json({ id: String(post.id) });
}
const post = await db.post.create({
data: {
title: body.title,
slug: body.slug,
html: body.content,
coverUrl: body.coverImageUrl,
publishedAt: new Date(body.publishedDate)
}
});
return NextResponse.json({ id: String(post.id) });
}Gövde limitini yükseltin
5mb civarına çekin, aksi halde istekler sessizce 413 ile düşer.Test etme
Panel → SEO → Ayarlar → Yayın Entegrasyonu ekranındaki Test İsteği Gönder düğmesi, gerçek yayınla birebir aynı isteği gönderir. Ayrı bir "test" bayrağı yoktur — bu kasıtlıdır, entegrasyon gerçek koşulda sınanır.
- 1
Test yazısı gönderilir
Başlığı "Seomio Test Yazısı" olan, lorem ipsum gövdeli gerçek bir içerik yollanır. Endpoint'iniz kabul ederse sitenizde gerçek bir yazı oluşur. - 2
Sonuç HTTP koduyla gösterilir
Panelde durum kodu, süre, gönderilen gövde ve aldığınız yanıt görünür; ayrıca yanıttaiddönüp dönmediği ayrıca belirtilir. - 3
Test yazısını otomatik sildirin
Test sonrası temizle seçeneği işaretliyse, oluşan test yazısı aynı endpoint'eaction: "delete"gönderilerek geri kaldırılır. Böylece tek testte hem yayın hem silme sözleşmesi doğrulanmış olur.
Temizlik için id şart
id dönmüş olması gerekir. Dönmediyse hangi kaydın silineceği bilinemez ve test yazısı sitenizde kalır.Güvenlik
Endpoint'iniz internete açık bir yazma ucudur; birkaç basit kural yeterlidir.
- Anahtarı her istekte doğrulayın.
Authorizationbaşlığı beklediğiniz değerle birebir eşleşmiyorsa401dönün — silme dalında da aynı kontrol geçerli olmalı. - Yalnızca HTTPS kullanın. Anahtar düz metin olarak başlıkta gider.
- Anahtarı kodda tutmayın. Ortam değişkeninde saklayın; sızdığından şüphelenirseniz panelden yeni bir değer girip endpoint'inizde güncelleyin.
- İçeriği güvenilir kabul edebilirsiniz ama yine de kendi şablonunuzda çıktı alırken HTML'i beklediğiniz alanlara yerleştirin; site genelinde script çalıştıran alanlara doğrudan enjekte etmeyin.
- Silme dalını sınırlayın. Yalnızca Seomio'nun oluşturduğu kayıtları silin (kayıtlarınızda bir
seomio_refalanı tutmak en temiz yoldur); böylece yanlış birexternalIdsitenizdeki başka bir içeriği silemez.
Sorun giderme
Panelde gördüğünüz duruma göre en olası nedenler.
| Belirti | Olası neden ve çözüm |
|---|---|
| 401 / 403 | Anahtar eşleşmiyor ya da biçim yanlış. Panelde Bearer token mı Ham token mı seçili kontrol edin; anahtarın başında/sonunda boşluk bırakmayın. |
| 404 | Endpoint yolu hatalı. Panele yazdığınız adresi tarayıcıda değil, doğrudan curl ile deneyin. |
| 405 | Adres POST kabul etmiyor. Rotanızı POST olarak tanımlayın; güncelleme ve silme de POST ile gelir. |
| 415 | Sunucu application/json gövdeyi ayrıştırmıyor. JSON body parser'ı etkinleştirin. |
| 413 / boş gövde | Gövde limiti düşük. HTML içerik birkaç yüz KB olabilir, limiti 5mb civarına çıkarın. |
| Aynı yazı tekrar tekrar oluşuyor | Create yanıtında id döndürmüyorsunuz. Bkz. Beklenen yanıt. |
| Sildiğim yazı sitede kalıyor | Endpoint action: "delete" dalını işlemiyor ya da entegrasyon ayarında syncDeletes: false. Bkz. Silme. |
| Görseller görünmüyor | Kapak ve gövde görselleri CDN üzerinde mutlak URL'dir; şablonunuzda göreli yola çevirmeyin. |
| İstek hiç ulaşmıyor | Güvenlik duvarı/WAF Seomio'nun isteğini engelliyor olabilir. Sunucu erişim loglarına bakın; yönlendirme varsa nihai adresi panele yazın. |