Mert Özen Her Satırda Daha İleri
SEO ve GEO Structured Data ve Schema.org — #03

BlogPosting, BreadcrumbList ve Yazar Entity'si: Person, ProfilePage, Organization ve WebSite ile Entity Graph Kurmak

Mert Özen 14 Kas 2026 11 dk 3 görüntülenme
BlogPosting, BreadcrumbList ve Yazar Entity'si: Person, ProfilePage, Organization ve WebSite ile Entity Graph Kurmak

Bir teknik blog'un structured data'sı, sitede tek bir @id ile tanımlanan Person, Organization ve WebSite'a bağlanan bir BlogPosting ile dil başına doğru URL'li bir BreadcrumbList'ten oluşur. Google'ın Article, Breadcrumb ve ProfilePage önerilerini, dateModified'ı dürüst kullanmayı ve Schema Markup Validator'da graph'ın nasıl çözüldüğünü .NET 10 ile gösteriyor, mertozen.com'un canlı çıktısını denetliyoruz.

Sürüm notu: Bu yazı Google'ın Article, Breadcrumb, Profile page, Organization ve Site names dokümanlarının Kasım 2026 hâlini ve schema.org 30.1'i esas alır; kodlar .NET 10 ile derlenip denenmiştir.

Önce Tahmin Et

mertozen.com'daki herhangi bir yazının JSON-LD'sinde (JSON for Linking Data; bağlantılı veriyi JSON ile yazma standardı) yazar ve yayıncı aynı kişi: Mert Özen. Ana sayfada da, Hakkımda sayfasında da bir Person düğümü var. Soru: Google bu üç sayfadaki dört Person'ın aynı kişi olduğunu nereden bilir? İkinci soru: Bir yazının JSON-LD'sinde dateModified değeri datePublished'dan önce olabilir mi, olursa ne anlama gelir? Üçüncüsü: İngilizce bir yazının breadcrumb'ı Türkçe sayfalara link verirse bu bir hata mı?

Üç sorunun da cevabı bu sitenin canlı çıktısında. Önce Bookstore'da doğru graph'ı kuracak, sonra aynı gözle mertozen.com'a bakacağız.

BlogPosting'de Google'ın Önerdikleri

Google'ın Article dokümanında zorunlu property yok. Bunun yerine sayfaya uyan property'leri eklemeni öneriyor ve Article, NewsArticle, BlogPosting için aynı listeyi veriyor: author (ve onun name ile url alanları), datePublished, dateModified, headline ve image. "Zorunlu değil" demek "önemsiz" demek değil: bu property'ler Google'ın yazarı, tarihi ve kapak görselini sayfanın geri kalanından ayırt etmesine yardım ediyor ve birbirleriyle, görünen sayfayla tutarlı olmaları gerekiyor.

Dokümandaki ayrıntılar bir geliştiricinin koduna doğrudan dönüşüyor:

  • author: sayfada görünen her yazar ayrı bir Person ya da Organization olarak, name yalnızca adı taşır; ünvan jobTitle'a, yayıncı adı publisher'a gider.
  • author.url: yazarı tek başına tanımlayan bir sayfa (yazar sayfası, Hakkımda); sameAs da kabul ediliyor ve Google ikisini de yazarı ayırt etmek için kullanıyor.
  • image: yazıyla ilgili, crawl ve index edilebilir görseller; Google birden çok yüksek çözünürlüklü görseli (genişlik × yükseklik en az 50.000 piksel) 16x9, 4x3 ve 1x1 oranlarında vermeyi öneriyor.
  • Tarihler: ISO (International Organization for Standardization; uluslararası standart kuruluşu) 8601 biçiminde ve saat dilimiyle; saat dilimi yoksa Google Googlebot'un saat dilimini varsayıyor.

Bookstore'un graph builder'ı bunları tek bir method'da topluyor:

public JsonLdDocument Post(Post post, Author author, string lang)
{
    string url = PageUrl(lang, $"/blog/{post.Slug}");
    var posting = new BlogPostingNode(
        Id: $"{url}#article",
        Headline: post.Title.Get(lang),
        Description: post.Summary.Get(lang),
        Image: [$"{baseUrl}{post.ImagePath}"],
        DatePublished: post.PublishedAt,
        // yalnızca yayından sonraki gerçek bir düzenleme değişiklik sayılır
        DateModified: post.UpdatedAt > post.PublishedAt ? post.UpdatedAt : null,
        Author: [new IdRef(PersonId(author.Slug))],
        Publisher: new IdRef(OrganizationId),
        MainEntityOfPage: url,
        InLanguage: lang,
        IsPartOf: new IdRef(WebSiteId));

    var breadcrumb = new BreadcrumbListNode($"{url}#breadcrumb",
    [
        new(1, "Blog", PageUrl(lang, "/blog")),
        new(2, post.Category.Get(lang), PageUrl(lang, $"/blog/category/{post.CategorySlug}")),
        new(3, post.Title.Get(lang), null),
    ]);

    return new([posting, breadcrumb, Person(author, lang), Organization(), WebSite()]);
}

publisher Google'ın önerilen listesinde yok ama schema.org'da var ve Article dokümanı yayıncı adının author.name'e değil publisher'a yazılmasını istiyor. Bookstore bir şirket olduğu için yayıncı bir Organization.

dateModified'ı Dürüst Kullanmak

dateModified, yazının en son ne zaman değiştiğini söyler ve iki yönden kötüye kullanılabilir. Birincisi kasıtlı: içerik değişmediği hâlde tarihi bugüne çekmek. Google'ın helpful content sayfasındaki öz değerlendirme sorularından biri tam olarak bunu soruyor: içerik esaslı olarak değişmediği hâlde sayfaların tarihini taze görünsünler diye değiştiriyor musun? Bu soru "arama motoru için içerik üretmekten kaçın" başlığı altında duruyor. İkincisi kazara: CMS'in (Content Management System; içerik yönetim sistemi) "kayıt güncellendi" zamanını, bir yazım hatası düzeltmesini ya da otomatik bir işlemi "yazı değişti" diye yayınlamak.

Google'ın byline tarihleri dokümanı ayrıca görünen tarihle structured data'daki tarihin tutarlı olmasını, gelecekte bir tarih verilmemesini ve tarihin sayfada açıkça "Yayın" veya "Güncelleme" gibi etiketlenmesini istiyor. Bookstore bunu iki kuralla uyguluyor: UpdatedAt, PublishedAt'ten sonra değilse dateModified hiç yazılmıyor (yukarıdaki koddaki koşul) ve sayfadaki "Güncelleme" etiketi de aynı koşulla gösteriliyor. Böylece markup ile görünen sayfa aynı veriden ve aynı kuraldan çıkıyor.

Breadcrumb, sayfanın sitedeki yerini bir yol olarak gösterir. Google'ın dokümanına göre BreadcrumbList'in itemListElement'i en az iki ListItem içermeli; her ListItem'da position ve name zorunlu, item ise o adımın URL'i. Son adım için item gerekmiyor; verilmezse Google sayfanın kendi URL'ini kullanıyor. Doküman breadcrumb'ın URL yapısını kopyalamasını değil, kullanıcının sayfaya tipik olarak ulaştığı yolu göstermesini öneriyor ve en üstteki domain ile sayfanın kendisi için ayrı bir adım istemiyor.

Bir değişiklik daha var: Google 22 Ocak 2025'te dokümana breadcrumb'ların yalnızca masaüstü sonuçlarda gösterildiğini ekledi; küçük ekranlarda kesildiği için mobilde daha az faydalı bulmuş. Yani bu markup mobil sonuçta bir şey göstermiyor, ama masaüstünde ve Google'ın sayfanın yerini anlamasında hâlâ işe yarıyor.

İngilizce Bookstore yazısının breadcrumb'ı:

    {
      "@type": "BreadcrumbList",
      "@id": "https://bookstore.example/en/blog/csharp-books-for-beginners#breadcrumb",
      "itemListElement": [
        {
          "@type": "ListItem",
          "position": 1,
          "name": "Blog",
          "item": "https://bookstore.example/en/blog"
        },
        {
          "@type": "ListItem",
          "position": 2,
          "name": "Book Picks",
          "item": "https://bookstore.example/en/blog/category/book-picks"
        },
        {
          "@type": "ListItem",
          "position": 3,
          "name": "5 C# Books for Beginners"
        }
      ]
    },

Her adımın URL'i /en önekli; İngilizce yazının hiyerarşisi İngilizce sayfalardan oluşuyor. Çok dilli bir sitede bu ayrıntı kolay kaçar. Breadcrumb URL'lerini dil başına doğru kurmak, Teknik SEO (arama motorları için optimizasyonun teknik tarafı) serisinin hreflang yazısında anlattığımız URL stratejisinin bir parçası: her dil sürümü kendi URL'leriyle kendi içinde tutarlı olmalı.

Entity Graph: Tek Person, Tek @id

Bir yazının JSON-LD'si tek başına doğru olabilir ama sitenin geneline bakınca dağınık kalabilir. Yazar her sayfada ayrı bir anonim Person olarak yazılıyorsa, Google'ın bunların aynı kişi olduğunu anlaması ad benzerliğine ve link'lere kalır. @id bu belirsizliği kaldırıyor: site genelinde her kalıcı varlığa tek bir kimlik verilir ve her sayfa ona aynı kimlikle referans verir.

// Kararlı @id değerlerini ve her sayfa tipinin node'larını üretir
public sealed class SiteGraph(string baseUrl)
{
    public string OrganizationId => $"{baseUrl}/#organization";
    public string WebSiteId => $"{baseUrl}/#website";
    public string PersonId(string authorSlug) => $"{baseUrl}/authors/{authorSlug}#person";

    public static string Prefix(string lang) => lang == "en" ? "/en" : "";

    public string PageUrl(string lang, string path) => $"{baseUrl}{Prefix(lang)}{path}";

    public OrganizationNode Organization() =>
        new(OrganizationId, "Bookstore", $"{baseUrl}/", $"{baseUrl}/images/logo-512.png",
            ["https://github.com/bookstore-example"]);

    public WebSiteNode WebSite() =>
        new(WebSiteId, "Bookstore", $"{baseUrl}/", ["tr", "en"], new IdRef(OrganizationId));

    public PersonNode Person(Author author, string lang) =>
        new(PersonId(author.Slug), author.Name, PageUrl(lang, $"/authors/{author.Slug}"),
            author.JobTitle.Get(lang), new IdRef(OrganizationId), author.SameAs);

    public JsonLdDocument Home() => new([Organization(), WebSite()]);

Bookstore'da dört kalıcı varlık var. Organization (Bookstore şirketi) ve WebSite (site), kimliklerini kök URL'e bağlı fragment'lerden alıyor. Google'ın Site names dokümanı WebSite markup'ını yalnızca ana sayfaya, domain ya da subdomain kökünde koymayı yeterli buluyor; name ve url zorunlu, alternateName kısa ad için öneriliyor. Organization dokümanı da zorunlu property tanımlamıyor; ana sayfaya ya da "Hakkımızda" gibi tek bir sayfaya konmasını öneriyor ve logo'nun hangi logonun gösterileceğini anlamaya yardım ettiğini söylüyor.

Person'ın kimliği yazar sayfasına bağlı ve dilden bağımsız: TR ve EN sayfalarda Ada Kaplan aynı @id ile geçiyor, yalnızca url o dildeki yazar sayfasını gösteriyor. Yazar sayfası ise bir ProfilePage:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "ProfilePage",
      "@id": "https://bookstore.example/en/authors/ada-kaplan#profilepage",
      "url": "https://bookstore.example/en/authors/ada-kaplan",
      "inLanguage": "en",
      "isPartOf": {
        "@id": "https://bookstore.example/#website"
      },
      "mainEntity": {
        "@id": "https://bookstore.example/authors/ada-kaplan#person"
      }
    },
    {

Google'ın Profile page dokümanı bu tipi bir kişiye ya da kuruluşa odaklanan sayfalar için tanımlıyor ve geçerli örnekler arasında bir haber sitesindeki yazar sayfasını ve bir blog'un "Hakkımda" sayfasını açıkça sayıyor. Zorunlu olan mainEntity ve onun name'i (ya da alternateName'i); sameAs, image ve description öneriliyor. Bookstore'da mainEntity, yazının author'ıyla aynı @id: graph'ın bağlandığı yer burası.

Kişisel bir site için soru farklı: yayıncı bir Organization mı, Person mı? schema.org'da publisher ikisini de kabul ediyor. Arkasında şirket olmayan kişisel bir blog'da yayıncıyı yazarla aynı Person yapmak dürüst olan; o zaman author ve publisher aynı @id'ye işaret eder ve kişi bir kez, tam olarak tanımlanır.

Markup Yazarlık Kanıtı Üretmez

sameAs ile GitHub, LinkedIn ve YouTube profillerini eklemek, "Ada Kaplan" adındaki başka insanlardan bu kişiyi ayırt etmeye yarar. Ama markup bir iddiadır, kanıt değil. SEO Temelleri serisinin "Google Nasıl Sıralar? Ranking System'ler, Core Update'ler ve E-E-A-T" yazısında konuştuğumuz deneyim, uzmanlık, otorite ve güven sinyalleri, Google'ın kendi dokümanlarında sayfanın görünen içeriği ve sitenin itibarı üzerinden anlatılıyor. Görünen bir byline, yazarı gerçekten anlatan bir yazar sayfası ve o sayfadan gerçek profillere giden link'ler olmadan JSON-LD'deki Person boş bir kutudur. Markup'ın işi, zaten görünen bu bilgiyi makinenin yanlış anlamamasını sağlamak.

Schema Markup Validator'da Graph Nasıl Görünür?

Bookstore yazısının ilk sürümünde BlogPosting, isPartOf ile WebSite'a referans veriyordu ama WebSite düğümü yalnızca ana sayfada tanımlıydı. Sayfanın HTML'ini schema.org'un Schema Markup Validator'ına verdik ve sonucu düz metne döktük:

objects=3 errors=0 warnings=0
  BreadcrumbList  https://bookstore.example/blog/csharp-books-for-beginners#breadcrumb
  CreativeWork    https://bookstore.example/#website
  BlogPosting     https://bookstore.example/blog/csharp-books-for-beginners#article  author -> Person, publisher -> Organization

Validator author ve publisher'ı @id üzerinden doğru birleştirmiş: ayrı düğümler olarak yazdığımız Person ve Organization, BlogPosting'in altında görünüyor. Ama tanımı sayfada olmayan WebSite kimliği, tipi bilinmediği için isPartOf'un beklenen tipinden türetilmiş çıplak bir CreativeWork olarak ayrı bir öğe olmuş. Hata ya da uyarı yok, ama graph yarım. WebSite düğümünü yazı sayfasının graph'ına da ekleyince:

objects=2 errors=0 warnings=0
  BreadcrumbList  https://bookstore.example/blog/csharp-books-for-beginners#breadcrumb
  BlogPosting     https://bookstore.example/blog/csharp-books-for-beginners#article  author -> Person, publisher -> Organization, isPartOf -> WebSite

İki öğe, sıfır hata, sıfır uyarı ve bütün bağlar çözülmüş. Bu, ikinci yazıda söylediğimiz kuralın pratikteki karşılığı: bir sayfada referans verdiğin düğümü o sayfada da tanımla.

mertozen.com'u Aynı Gözle Denetlemek

Aynı kontrolleri bu sitenin canlı çıktısına uyguladık. İlk yazıdaki GraphDump aracını İngilizce bir yazıya karşı çalıştırıp ilgili satırları süzünce:

$ dotnet run -- https://www.mertozen.com/en/blog/title-link-snippet-meta-description https://www.mertozen.com | grep -E 'datePublished|dateModified|  a  "Person"|hakkimda|  item  '
_:b0  datePublished  "2026-11-08T11:04:00.0000000Z"
_:b0  dateModified  "2026-11-08T06:55:22.8866667Z"
_:b1  a  "Person"
_:b1  url  "https://www.mertozen.com/hakkimda"
_:b2  a  "Person"
_:b2  url  "https://www.mertozen.com/hakkimda"
_:b6  item  "https://www.mertozen.com"
_:b7  item  "https://www.mertozen.com/blog"
_:b8  item  "https://www.mertozen.com/blog?categ..."
_:b9  item  "https://www.mertozen.com/blog?serie..."
_:b10  item  "https://www.mertozen.com/en/blog/ti..."

Bulgular, sitenin Helpers/JsonLdBuilder.cs ve controller kodu ile birlikte:

BulguKanıtEtkisi
Hiçbir düğümde @id yokBütün düğümler _:b ile başlayan anonim düğümler; yazar ve yayıncı iki ayrı PersonAynı kişi sayfa içinde iki, sitede birçok anonim kopya
Person.url sayfaya göre değişiyorAna sayfada site kökü, yazı ve Hakkımda'da /hakkimdaKişinin kimliği tutarsız tarif ediliyor
dateModified yayından önce06:55:22Z değişiklik, 11:04:00Z yayınController'daki "güncelleme yayından sonraysa" koşulu article:modified_time'a uygulanıyor, JSON-LD'ye uygulanmıyor
EN yazının breadcrumb'ı TR sayfalara gidiyoritem değerleri /en öneksiz; EN author.url de /hakkimdaEN hiyerarşi TR URL'lerle kurulmuş
Ana sayfada SearchActionWebSite.potentialActionSitelinks search box Kasım 2024'te kaldırıldı; zararsız ama işe yaramıyor

Sağlam taraflar da var. Schema Markup Validator canlı yazının HTML'inde iki öğe buldu ve sıfır hata, sıfır uyarı verdi:

objects=2 errors=0 warnings=0
  BreadcrumbList
  BlogPosting       author -> Person, publisher -> Person, isPartOf -> CreativeWorkSeries, speakable -> SpeakableSpecification

headline, image, tarihler, author.url ve sameAs dolu; görseller mutlak URL; breadcrumb dört adım ve son adım yazının kendi URL'i. Google açısından Article için zorunlu property olmadığından bu yazı Article markup'ı bakımından eksik değil. Google'ın kendi uygunluk görüşü için aynı URL'i Rich Results Test'te ve Search Console'daki URL Inspection'da çalıştırmak gerekiyor; bu yazı için o testi çalıştıramadık ve sonucunu tahmin etmiyoruz. Hangi bulgunun düzeltileceğine site sahibi karar verecek. En ucuz ve en etkili adım, dateModified'a controller'daki koşulun aynısını uygulamak ve sitede tek bir Person @id'si tanımlamak.

Üretimde Dikkat

@id değerlerini kod içinde tek bir yerde üret. Bookstore'da bütün kimlikler SiteGraph'ın property'lerinden geliyor; bir sayfanın elle "/#org" yazması, graph'ı sessizce ikiye böler.

Görünen tarihle markup'taki tarihi aynı kuraldan üret. mertozen.com'daki dateModified sorunu tam olarak iki ayrı yerde iki ayrı kural olmasından doğuyor: biri meta tag için, biri JSON-LD için.

Dil sürümlerini ayrı ayrı denetle. TR sayfada her şey doğru olabilir ve EN sayfa yine de TR URL'ler taşıyabilir. Serinin son yazısındaki testler bu yüzden her iki dil için çalışıyor.

Sık Düşülen Tuzaklar

Yazarı düz metin olarak yazmak. "author": "Ada Kaplan" schema.org'a göre kabul edilebilir, ama Google'ın Article dokümanı tip ve url (ya da sameAs) taşıyan bir Person istiyor.

name alanına ünvan ya da ek koymak. "Dr. Ada Kaplan, Bookstore Editörü" gibi bir değer, adı ünvan ve kuruluşla karıştırır. Ünvan jobTitle, kuruluş worksFor ya da publisher.

Breadcrumb'da URL'i kopyalamak. /blog/2026/11/csharp-books gibi bir yol, "2026" ve "11" adımları üretir; okurun izlediği yol "Blog, Kitap Önerileri, yazı".

Her sayfaya Organization ve WebSite'ın tamamını koymayı zorunlu sanmak ya da hiç koymamak. Google bu ikisini ana sayfada istiyor; yazı sayfasında referans verdiğin için tanımlarsan graph tamamlanır, tanımlamazsan validator'da çıplak bir düğüm görürsün. İkisi de hata değil, ama birincisi daha açık.

Küçük Bir Deneme

★☆☆ Kendi blog'unun bir yazısında yazarın kaç kez ve hangi url ile tanımlandığını say. Ana sayfa ve yazar sayfası ile karşılaştır.

★★☆ Bookstore'a ikinci bir yazar ekle ve bir yazıyı iki yazarlı yap. author'ın iki @id içeren bir array olduğunu ve her iki Person'ın da graph'ta tanımlandığını doğrula.

★★★ GraphDump'a, aynı @type ve aynı name'e sahip ama @id'si olmayan birden çok düğümü "muhtemel kopya" olarak raporlayan bir kontrol ekle ve mertozen.com'un bir yazısında çalıştır.

Bir teknik blog'un temel graph'ı artık yerinde. Bir sonraki yazıda bu çekirdeğin dışında kalan tiplere bakıyoruz: VideoObject, SoftwareApplication ve speakable, ve 2023'ten bu yana Google'ın kaldırdığı rich result'ların, özellikle FAQ (Frequently Asked Questions; sık sorulan sorular) ve HowTo'nun hikâyesi.