Cloudflare Agent-Readiness ile GEO Optimize Rehberi
Cloudflare Agent-Readiness skorumuzu 25/100'den 83/100'e taşıdık; Level 1'den Level 4'e çıktık. Content Signals, llms.txt, MCP Server Card, Agent Skills, WebMCP, Markdown Negotiation ve API Catalog — tüm değişikliklerin teknik hikayesi ve karşılaştığımız tuzaklar.

Bir Pazar gecesi shobi.ai'yi isitagentready.com'da tarattığımızda 25/100, Level 1 "Basic Web Presence" gördük. Ertesi sabah aynı taramada 83/100, Level 4 "Agent-Integrated" vardı. Arasında geçen 12 saatte uyguladığımız her şeyi — Content Signals'tan WebMCP'ye, karşılaştığımız Railway/Cloudflare proxy tuzaklarına kadar — bu yazıda paylaşıyoruz.
GEO (Generative Engine Optimization), bir sitenin yalnız insan ziyaretçilere değil, ChatGPT, Claude.ai, Perplexity ve Google AI Overviews gibi AI ajanlarına ne kadar "okunabilir" olduğunu ölçüyor. Geleneksel SEO Google'ın HTML'i parse etmesini hedeflerken, GEO sitenin AI tarafından alıntılanabilir, araç olarak tüketilebilir, otonom olarak işlem yapılabilir olmasını hedefliyor. Shobi'nin müşterilerine sattığı bir özellik bu; ama kendi landing'imiz Level 1'di. Dogfooding zamanı geldi.
I. Ölçüm: isitagentready.com ve CF formülü
Cloudflare'in geliştirdiği isitagentready.com, bir URL'yi 5 kategoride 19 farklı kontrolden geçirir: Discoverability, Content Accessibility, Bot Access Control, Discovery, Commerce. Tarama API'si açık:
curl -X POST https://isitagentready.com/api/scan \
-H "Content-Type: application/json" \
-d '{"url":"shobi.ai"}'
Skor formülü basit ama incelikli: commerce kategorisi (e-ticaret protokolleri x402, ACP vb.) skor dışında, a2aAgentCard ve ap2 de hariç tutuluyor, neutral durumlar yok sayılıyor. Geri kalan check'lerden pass/fail oranı 0-100 skoru veriyor.
Shobi'nin SaaS panelinde de bu API'yi geo_scan.py servisi üzerinden tüketiyoruz — müşterilerimizin storefront'larını aynı formülle puanlıyoruz. Yani kendi landing'imizi taramak, kendi ürünümüze inanma testi.
Başlangıç: %25, Level 1
| Geçen check (3) | Kalan check (9) |
|---|---|
| robotsTxt | linkHeaders |
| sitemap | markdownNegotiation |
| robotsTxtAiRules (wildcard) | contentSignals |
| apiCatalog | |
| oauthDiscovery | |
| oauthProtectedResource | |
| mcpServerCard | |
| agentSkills | |
| webMcp |
Hedef: Level 3 (Discovery-Ready) ve %70+ skor. Bunu Faz 1'de yaptık. Faz 2'de Level 4'e tırmandık.
II. Faz 1: Bot-Aware'den Discovery-Ready'e
İlk turda yedi check'i pass'e çevirdik. Aşağıdaki sıra etki büyüklüğüne göre.
1. Content Signals — robots.txt'e tek satır
contentsignals.org standardı, robots.txt'e yapay zeka eğitimi, arama ve LLM input kullanımına dair sahip tercihini deklare etmemizi öneriyor. Next.js'in MetadataRoute.Robots API'si bu direktifi desteklemediğinden, app/robots.ts'yi silip public/robots.txt static dosyaya geçtik:
# shobi.ai
User-Agent: *
Allow: /
Content-Signal: ai-train=no, search=yes, ai-input=yes
Host: https://shobi.ai
Sitemap: https://shobi.ai/sitemap.xml
Değer kararı: model eğitimine izin yok (ai-train=no), AI arama ve alıntılama serbest (search=yes), kullanıcı sorusuna bağlam olarak kullanılabilir (ai-input=yes). Bu Shobi'nin Shopify müşterilerine "Shobi ile Tamamla" ile bastığı default ile aynı.
2. HTTP Link Headers ve next.config.ts
Discoverability check'i homepage HTTP yanıtında Link header'ı arıyor (alternatives işaretliyor). Static export modunda headers() config'i çalışmıyordu; server mode'a geçtik:
const nextConfig: NextConfig = {
output: "standalone", // önceden "export"
trailingSlash: true,
async headers() {
return [{
source: "/(.*)",
headers: [
{ key: "Link", value:
'</sitemap.xml>; rel="sitemap", ' +
'</llms.txt>; rel="alternate"; type="text/plain", ' +
'</.well-known/mcp/server-card.json>; rel="alternate"; type="application/json"'
},
{ key: "X-Robots-Tag", value: "index, follow" },
],
}];
},
};
Bu tek satırlık config Faz 2'de WebMCP ve markdown negotiation için gerekli olan API route'larını ve middleware'i mümkün kıldı.
3. JSON-LD Genişletme: Organization → SoftwareApplication → FAQPage
Layout.tsx'te Organization + WebSite zaten vardı, ama @id ile bağlı değildi, social links ve contactPoint yoktu. Üçüncü bir SoftwareApplication ekledik (Shobi'nin kendisini schema.org ile tanımlıyor):
{
"@type": "SoftwareApplication",
"applicationCategory": "BusinessApplication",
"applicationSubCategory": "E-Commerce Management",
"featureList": [
"Trendyol, Hepsiburada, N11, Amazon TR, Shopify, ikas entegrasyonu",
"Rakip fiyat ve reklam takibi",
"Google Ads + Meta Ads otomasyonu",
"AI Agent ile doğal dilde sorgu",
"GEO ve 'Shobi ile Tamamla'"
],
"audience": { "@type": "BusinessAudience", "audienceType": "E-Ticaret İşletmeleri" }
}
Bir de app/page.tsx'e FAQPage schema ekledik — yedi soru-cevap. Tek kaynak (app/lib/faq.ts) hem visible accordion UI'ı hem de schema'yı besliyor; insan ve AI aynı içeriği görüyor.
Blog tarafında [slug]/page.tsx'in BlogPosting'ine wordCount, articleBody (sanitize edilmiş 600 char snippet), timeRequired, keywords ve Person author eklendi. Blog index'ine CollectionPage + ItemList, tag sayfalarına da CollectionPage + Breadcrumb.
4. llms.txt + llms-full.txt
llmstxt.org standardı, sitenin LLM-friendly bir özetini ve tüm önemli URL'lerini içeren bir /llms.txt dosyası önerir. İki dosya ekledik:
/llms.txt— kısa indeks (1.9KB): hero özet + blog listesi + discovery URL'leri/llms-full.txt— landing'in tam Markdown metni (~6KB): hero, özellikler, entegrasyonlar, fiyatlandırma, ekip, SSS
Bu ikinci dosya Faz 2'de markdown content negotiation için de gerekli oldu — bir dosyadan iki kazanım.
5. MCP Server Card — Şema farkı tuzağı
/.well-known/mcp/server-card.json ile sitenin MCP yeteneğini tanımlıyoruz. İlk denemede top-level name field'ı kullandık — CF scanner fail dedi: "missing required fields (need serverInfo.name or name)". Tuhaf, name zaten vardı. CF'in kendi referansını çekince anladık ki şema farklı:
{
"$schema": "https://modelcontextprotocol.io/schemas/server-card.json",
"serverInfo": {
"name": "Shobi Public MCP",
"version": "1.0.0"
},
"description": "...",
"url": "https://shobi.ai/mcp/public/",
"transport": { "type": "http" },
"capabilities": { "tools": true }
}
Ders: "Cloudflare'in scanner'ı kendi schema beklentisini örnek dosyaya gömmüştür" — referans olarak https://isitagentready.com/.well-known/mcp/server-card.json açıp birebir kopyalamak en kısa yol.
6. Agent Skills Index
/.well-known/agent-skills/index.json ile sitenin AI ajanlarına sunduğu becerileri listeliyoruz. Üç skill ekledik: shobi-overview, shobi-blog, shobi-integrations. Her birinin altında bir SKILL.md Markdown dosyası — AI ajanın okuyup öğrenebileceği uzun-form içerik.
7. Public MCP endpoint: /mcp/public/
Server mode'a geçişin getirdiği ikinci kazanım: gerçek bir API route. JSON-RPC 2.0 ile dört tool yayınlıyoruz:
// app/mcp/public/route.ts (özet)
const TOOLS = [
{ name: "search_blog", description: "Blog yazılarını arar...", inputSchema: {...} },
{ name: "get_features", description: "Shobi özelliklerini döner", inputSchema: {} },
{ name: "get_integrations", description: "Desteklenen entegrasyonlar", inputSchema: {} },
{ name: "request_demo", description: "Demo talebi kaydet", inputSchema: {...} },
];
POST /mcp/public/ { jsonrpc: "2.0", method: "tools/call", params: { name: "search_blog", arguments: { query: "MCP", limit: 3 }}} çağırırsanız blog'da MCP geçen yazıları JSON formatta döndürüyor. CF scan bu endpoint'i mcpServerCard.endpoints[0].url olarak bulup ping atıyor.
Faz 1 sonuç
| Metrik | Önce | Sonra |
|---|---|---|
| Skor | %25 | %58 |
| Level | 1 (Basic Web Presence) | 2 (Bot-Aware) |
| Pass | 3 | 7 |
Eklenen geçişler: linkHeaders, contentSignals, mcpServerCard, agentSkills.
III. Faz 2: Agent-Readable'dan Agent-Integrated'a
İkinci turda üç check daha pass'e çevirdik ve Level 4'e ulaştık. Üçüncü check (markdownNegotiation) production'da middleware bug'ıyla iki tur sürdü; lessons learned aşağıda.
8. Markdown Content Negotiation — middleware'in production tuzağı
CF scan homepage'e Accept: text/markdown header'ıyla GET atıyor; yanıt Content-Type: text/markdown ile markdown gövde olmalı. HTML default kalıyor.
İlk denememiz Next.js middleware'inde aynı origin'e fetch atmaktı:
const upstream = await fetch(new URL("/llms-full.txt", req.nextUrl.origin));
const body = await upstream.text();
return new NextResponse(body, { headers: { "content-type": "text/markdown" }});
Local'de 200, production'da 500. Sebep: Edge runtime middleware'inin Railway/Cloudflare proxy katmanından geri kendi origin'ine fetch atması. Ayrıca trailingSlash: true config'i NextResponse.rewrite hedef path'ine de / ekleyip 308 yapıyordu.
Çözüm: Markdown içeriğini runtime'da çekmek yerine build-time inline import. Webpack asset/source rule ile public/llms-full.txt'i string olarak middleware bundle'ına yerleştirdik:
// next.config.ts (webpack içine)
config.module.rules.push({
test: /\.(txt|md)$/,
type: "asset/source",
});
// middleware.ts
import homeMarkdown from "./public/llms-full.txt";
import blogIndexMarkdown from "./public/llms.txt";
export function middleware(req: NextRequest) {
const accept = req.headers.get("accept") ?? "";
if (!accept.includes("text/markdown")) return NextResponse.next();
const path = req.nextUrl.pathname;
let body: string | null = null;
if (path === "/" || path === "") body = homeMarkdown;
else if (path === "/blog" || path === "/blog/") body = blogIndexMarkdown;
if (!body) return NextResponse.next();
return new NextResponse(body, {
status: 200,
headers: {
"content-type": "text/markdown; charset=utf-8",
"cache-control": "public, max-age=300",
"x-markdown-tokens": String(Math.ceil(body.length / 4)),
vary: "accept",
},
});
}
Runtime fetch yok. trailingSlash etkilemiyor. Bonus: blog yazıları için ayrı bir app/blog/[slug]/llms.md/route.ts SSG handler — Accept: text/markdown ile bir blog URL'ine gidildiğinde frontmatter + content text/markdown olarak servis ediliyor.
Ders: Edge runtime middleware'inde same-origin fetch güvenilmez. Production'da çalışacağına emin değilseniz, fetch yerine build-time inline content yapın.
9. WebMCP — navigator.modelContext.registerTool()
Tarayıcıdaki AI ajan extension'ları (Claude in Chrome, ChatGPT Atlas, Comet) sayfaya bağlandığında navigator.modelContext API'sini sorguluyor. Sayfa tool'larını runtime'da kayıt ederse ajan onları kullanabiliyor. Resmi spec çok yeni (taslak 23 Nisan 2026).
WebMcpScript.tsx client component'i, MCP public endpoint'imizdeki dört tool'u tarayıcı tarafında kaydediyor:
"use client";
import { useEffect } from "react";
const TOOLS = [
{
name: "search_blog",
description: "Shobi blog yazılarını arar",
inputSchema: { type: "object", properties: { query: { type: "string" }, limit: { type: "number" }}},
execute: async (input) => callMcp("search_blog", input),
},
// get_features, get_integrations, request_demo...
];
async function callMcp(name: string, args: Record<string, unknown>) {
const res = await fetch("/mcp/public/", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0", id: Date.now(),
method: "tools/call", params: { name, arguments: args },
}),
});
const data = await res.json();
return data.result?.content?.[0]?.text ?? "";
}
export default function WebMcpScript() {
useEffect(() => {
if (!navigator.modelContext) return;
const controller = new AbortController();
for (const tool of TOOLS) {
navigator.modelContext.registerTool(tool, { signal: controller.signal });
}
return () => controller.abort();
}, []);
return null;
}
Layout.tsx'te <WebMcpScript /> body sonuna eklendi. CF scan headless Chrome'la sayfayı açıp navigator.modelContext'i sorguluyor — tools varsa pass.
10. API Catalog — RFC 9727 linkset
/.well-known/api-catalog ile sitenin programmatic API'lerini deklare ediyoruz. Content-Type application/linkset+json olmak zorunda:
// app/.well-known/api-catalog/route.ts
const LINKSET = {
linkset: [
{
anchor: "https://shobi.ai/mcp/public/",
"service-desc": [{ href: ".../server-card.json", type: "application/json" }],
"service-doc": [{ href: ".../llms-full.txt", type: "text/plain" }],
status: [{ href: ".../mcp/public/" }],
},
{
anchor: "https://shobi.ai/blog/",
"service-doc": [
{ href: ".../blog/rss.xml", type: "application/rss+xml" },
{ href: ".../llms.txt", type: "text/plain" },
],
},
],
};
export function GET() {
return new NextResponse(JSON.stringify(LINKSET, null, 2), {
status: 200,
headers: {
"content-type": "application/linkset+json",
"cache-control": "public, max-age=3600",
},
});
}
NextResponse.json content-type'ı application/json'a zorladığı için yeni Response ile manuel header. Detay önemli: RFC 9727 Appendix A tam format.
Faz 2 sonuç
| Metrik | Önce (Faz 1) | Sonra (Faz 2) |
|---|---|---|
| Skor | %58 | %83 |
| Level | 2 (Bot-Aware) | 4 (Agent-Integrated) |
| Pass | 7 | 10 |
Yeni pass'ler: apiCatalog, webMcp, markdownNegotiation. Tek turda iki seviye atladık.
IV. Kalan iki fail neden N/A
Skor 12/12 değil 10/12. Geri kalan iki check oauthDiscovery ve oauthProtectedResource — sitenin OAuth provider olduğunu varsayan kontroller. shobi.ai bir auth server değil; MCP endpoint'imiz auth gerektirmiyor (public, anonim erişim). Bu check'leri pass etmek için MCP'ye OAuth katmanı eklemek gerekir; ihtiyaç yok. Bizim için fiili tavan %83 = %100.
V. Aslında Shobi müşterilerimize de aynısını yapıyor
Bu yazıda anlattığımız her şey — Content Signals, MCP Server Card, Agent Skills, llms.txt, WebMCP script, JSON-LD enrich — Shobi panelindeki GEO sekmesinde tek tıkla müşterilerimize uygulanabilir. "Shobi ile Tamamla" butonu Claude Haiku ile mağazanızın eksik SEO/JSON-LD/MCP içeriğini otomatik üretip Shopify theme.liquid'inize, ikas storefront'unuza veya İdeasoft mağaza şablonunuza yazıyor.
İlerleme grafiği
| Aşama | Skor | Level | Pass | Eklenenler |
|---|---|---|---|---|
| Başlangıç | %25 | 1 | 3 | (mevcut robots, sitemap, AI wildcard) |
| Faz 1 | %58 | 2 | 7 | linkHeaders, contentSignals, mcpServerCard, agentSkills |
| Faz 2 | %83 | 4 | 10 | apiCatalog, webMcp, markdownNegotiation |
12 saatte +%58, +7 pass, +3 level. AI'nın internet'i okuma biçimi hızla değişiyor; siteniz hangi level'da?


