metrics İçerik API'si — v1
Sitelerin ve mobil uygulamaların, panelde yazılıp onaylanan blog yazılarını kendi tasarımlarıyla göstermesi için sözleşme. Site yalnız yayındaki yazıları okur (taslak, onaydaki sürüm ya da siteden kaldırılmış yazı hiç dönmez); metrics bir yazı değişince sitenin bildirim adresine imzalı bir istek (yayın webhook'u) gönderir, site önbelleğini yeniler ve yazıyı yeniden okur.
Bu sayfa derleme sırasında depodaki docs/api/content-api.md dosyasından üretilir.
- Taban adres:
https://api.<alan>/v1/content/apps/{appId}(geliştirme:http://localhost:7100/v1/content/apps/{appId}) appId: panelde İçerik → Ayarlar → Siteye bağlama sayfasında yazan uygulama kimliği- Makine tarafından okunur belge:
openapi/content.json(koddan üretilir)
1. Kavramlar
2. Kimlik doğrulama
GET /v1/content/apps/{appId}/posts HTTP/1.1
Host: api.<alan>
Authorization: Bearer mk_…
Accept-Language: tr- Anahtarsız istek
401,content:readkapsamı olmayan anahtar403alır. - Anahtarın uygulama kapsamı bu uygulamayı içermiyorsa ya da yazı yayında değilse
404döner (başka uygulamanın yazılarının varlığı sızmaz). - Hata gövdesi her zaman
{ "error": { "code", "message", "traceId" } };messageisteğin dilindedir (Accept-Language:tr,en).
3. Uçlar
Bütün yollar https://api.<alan>/v1/content/apps/{appId} altındadır.
4. Yazı listesi
GET /posts?feed=web&locale=tr&limit=20
{
"items": [
{
"id": "01a1b8a9-2ec3-7d45-a807-1949a3f6aa55",
"slug": "olcumleme-rehberi",
"locale": "tr",
"title": "Ölçümleme Rehberi: Sık Yapılan 5 Hata",
"excerpt": "Olay adlarından kampanya etiketlerine…",
"url": "https://www.ornek.com/blog/olcumleme-rehberi",
"category": { "slug": "rehberler", "name": "Rehberler" },
"tags": ["ölçüm", "kurulum"],
"authors": [{ "slug": "deniz-ornek", "name": "Deniz Örnek" }],
"cover": { "assetId": "…", "url": "…", "expiresAt": "…", "file": "…", "alt": "…", "width": 1920, "height": 1080 },
"feeds": ["web"],
"readingMinutes": 1,
"publishedAt": "2026-10-29T00:57:37.730Z",
"updatedAt": "2026-10-29T00:57:37.730Z",
"revision": 1
}
],
"nextCursor": null
}url, uygulamanın panelde girilen yazı adresi şablonundan (https://www.ornek.com/blog/{slug}) çıkar; şablon yoksa null.
5. Adresle yazı, yönlendirme ve kaldırılmış yazı
GET /posts/{slug} üç biçimde yanıt verir:
- API kendisi 301 vermez: HTTP istemcisi yönlendirmeyi sessizce izleyip eski adresi önbellekte tutmasın diye. Siteniz
redirectaldığında ziyaretçiye kalıcı yönlendirme (301) vermelidir; arama motorları yeni adrese geçer. 410alan sayfa ziyaretçiye «bu yazı kaldırıldı» demeli ve410durum koduyla dönmelidir; arama motorları yazıyı dizinden çıkarır.
Belgenin alanları:
6. Önbellek: ETag, Last-Modified, 304
Her yanıt ETag (gövdenin özeti) ve Last-Modified (uygulamanın sunulan içeriğinin son değişikliği) taşır. Sonraki istekte bunları geri gönderin; değişiklik yoksa yanıt 304 Not Modified ve gövdesizdir.
GET /v1/content/apps/{appId}/posts/olcumleme-rehberi HTTP/1.1
Authorization: Bearer mk_…
If-None-Match: "q1w2e3r4t5y6u7i8o9p0a1s2d3f4g5h6"
If-Modified-Since: Sun, 08 Nov 2026 01:00:00 GMT
HTTP/1.1 304 Not Modified- Yanıtlar
Cache-Control: private, max-age=60veVary: Authorization, Accept-Languagetaşır; önizlemeno-store. - Önerilen düzen: sayfaları uzun süre önbellekte tutun, yayın webhook'u geldiğinde ilgili sayfaları (yazının adresi, eski adresleri, listeler, site haritası) yenileyin; webhook kaçarsa
updatedSinceile artımlı eşitleme yapın.
7. Görseller
Kapak, paylaşım görseli, yazar fotoğrafı ve içerikteki görseller aynı biçimde gelir:
Görselleri sunucunuzda indirip kendi adresinizden sunun: imzalı bağlantılar süreli ve başka bir sitenin sayfasına doğrudan gömülmek için değildir. html içindeki <img src> adresleri de imzalı bağlantılardır; kendi kopyanızın adresiyle değiştirin.
8. Site haritası
GET /sitemap?limit=1000&feed=web her yazı için { slug, locale, url, updatedAt, alternates } döner (en çok 5000 girdi bir sayfada, imleçle). noindex yazılar dahil değildir; çeviriler alternates olarak gelir. Sitenizin sitemap.xml'ini bu listeden üretin.
9. Önizleme
Editörler panelden süreli bir önizleme bağlantısı (cp_… belirteci) oluşturabilir. Bağlantı metrics'in kendi betiksiz önizleme sayfasını açar. Sitenizin kendi tasarımıyla önizleme göstermek için kendi önizleme sayfanız belirteci GET /preview/{token} ucuna iletir: yanıt yazının çalışma sürümüdür (preview: true), no-store döner; süresi dolmuş ya da iptal edilmiş belirteç 410, tanınmayan belirteç 404 alır. Önizleme sayfasını noindex yapın.
10. Yayın webhook'u
Panelde İçerik → Ayarlar → Siteye bağlama sayfasında bir bildirim adresi eklediğinizde metrics şu durumlarda adrese POST gönderir:
Gövde küçüktür; yazının kendisini taşımaz, nerede okunacağını söyler:
{
"id": "01a1b8c0-…",
"type": "content.post.updated",
"createdAt": "2026-11-08T10:00:00.000Z",
"app": { "id": "01a1b249-…", "slug": "web-sitesi" },
"post": {
"id": "01a1b8a9-…",
"slug": "olcumleme-rehberi",
"previousSlugs": ["olcum-kurulumu-hatalari"],
"locale": "tr",
"url": "https://www.ornek.com/blog/olcumleme-rehberi",
"revision": 3,
"feeds": ["web"],
"publishedAt": "2026-10-29T00:57:37.730Z",
"updatedAt": "2026-11-08T10:00:00.000Z",
"documentPath": "/v1/content/apps/01a1b249-…/posts/olcumleme-rehberi"
}
}Başlıklar:
- İmzayı gelen ham gövde üzerinden doğrulayın,
t'nin 5 dakikadan eski olmadığını denetleyin; imza anahtarı (whsec_…) webhook oluşturulurken ya da yenilenirken panelde bir kez gösterilir. - 2xx dışındaki yanıtlar ve zaman aşımları (10 sn) 1, 5, 30 dakika ve 2, 6, 12 saat sonra yeniden denenir (en çok 24 saat); panelin teslim günlüğünden elle yeniden gönderilebilir. Aynı
idiki kez gelebilir: işlemiidile tekilleştirin. documentPathyazı siteden kalkıncanull'dır; o zaman yazının sayfasını410yapın.
import { createHmac, timingSafeEqual } from 'node:crypto';
/** Express ya da benzeri: gövde ham metin olarak alınır (JSON'a çevrilmeden). */
export function verifyMetricsSignature(rawBody, header, secret, now = Date.now()) {
const parts = Object.fromEntries(header.split(',').map((part) => part.trim().split('=')));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(now / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const given = Buffer.from(parts.v1 ?? '', 'hex');
return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, 'hex'));
}11. Hata kodları
12. Örnek: Next.js sitesinde blog sayfası
const API = `https://api.<alan>/v1/content/apps/${process.env.METRICS_APP_ID}`;
const headers = { authorization: `Bearer ${process.env.METRICS_CONTENT_KEY}`, 'accept-language': 'tr' };
export async function getPost(slug) {
const response = await fetch(`${API}/posts/${encodeURIComponent(slug)}`, { headers, next: { tags: [`post:${slug}`] } });
if (response.status === 410) return { gone: true };
if (response.status === 404) return { notFound: true };
if (!response.ok) throw new Error(`İçerik API'si: ${response.status}`);
const answer = await response.json();
if (answer.kind === 'redirect') return { redirect: { destination: `/blog/${answer.redirect.slug}`, permanent: true } };
return { post: answer.post };
}Webhook geldiğinde post:<slug> etiketini (ve previousSlugs'ın etiketlerini) yenileyin; liste sayfaları için ayrı bir etiket kullanın.