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

Organization Seviyesinde Mağaza Bilgisi: Logo, İletişim, MerchantReturnPolicy, Shipping Policy ve Öncelik Sırası

Mert Özen 15 Kas 2026 11 dk 6 görüntülenme
Organization Seviyesinde Mağaza Bilgisi: Logo, İletişim, MerchantReturnPolicy, Shipping Policy ve Öncelik Sırası

İade ve kargo politikası her ürün sayfasına ayrı ayrı yazılmaz; mağaza düzeyinde OnlineStore altında bir kez tanımlanır, ürün teklifleri ona @id ile işaret eder ve yalnızca istisnalar ürün düzeyinde ezilir. Bookstore için MerchantReturnPolicy ve ShippingService'i tek bir StorePolicy nesnesinden üretiyor, 14 günlük cayma hakkını ve Google'ın feed, Search Console ve markup arasındaki öncelik sırasını ele alıyoruz.

Sürüm notu: Bu yazı Google Search Central'ın Organization, Merchant return policy, Merchant shipping policy ve Merchant listing dokümanlarının 15 Kasım 2026'daki hâlini ve schema.org 30.1'i esas alır; kodlar .NET 10 (SDK 10.0.404), xUnit v3 4.0 ve AngleSharp 1.10 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) kargo ücretini 49,90 TL'den 59,90 TL'ye çıkardı. Ekip yeni ücreti kitap sayfalarının markup'ına yazmak için her ürünün Offer'ına bir shippingDetails ekliyor; yarım saat sonra binlerce kitabın yarısında eski, yarısında yeni ücret var, çünkü sayfaların bir kısmı cache'ten geliyor. Bu arada müşteri hizmetlerinden Gül, Search Console'a girip mağaza ayarlarına 14 günlük iade süresini girmiş. Soru: Google kargo ve iade bilgisini bu üç kaynaktan hangisinden alır ve ücret değişikliği kaç yere yazılmalıydı?

Önceki iki yazıda kitap sayfasının fiyatını, stoğunu ve puanını işaretledik. Bu yazıda mağazanın kendisine geçiyoruz: logo, iletişim, iade ve kargo politikası. Hepsini bir kez, mağaza düzeyinde tanımlıyoruz.

Organization Markup'ının Mağaza Tarafı

Serinin üçüncü yazısında Organization düğümü site adı ve logo için vardı; Google'ın Organization dokümanı da hâlâ zorunlu property tanımlamıyor ve ilgili olan her şeyi eklemeyi öneriyor. Satıcı için iş genişliyor: doküman, merchant knowledge panel'de iade politikası, adres ve iletişim bilgisinin bu markup'tan beslenebileceğini söylüyor. Önerilen property'ler arasında address, contactPoint, email, telephone, logo, legalName, sameAs ve kimlik alanları var: vatID, taxID, iso6523Code, duns, leiCode, naics, globalLocationNumber. DUNS (Dun & Bradstreet'in şirketlere verdiği numara) ve LEI (Legal Entity Identifier; tüzel kişi tanımlayıcısı) için Google artık ayrı alanlar yerine iso6523Code'u (0060: ve 0199: önekleriyle) öneriyor.

İki teknik öneri doğrudan Bookstore'u ilgilendiriyor. Birincisi tip: e-ticaret sitesi için Organization'ın en spesifik alt tipi olan OnlineStore öneriliyor (OnlineBusiness yerine); fiziksel mağaza varsa LocalBusiness'ın uygun alt tipi. İkincisi yer: bu bilgi ana sayfaya ya da kuruluşu anlatan tek bir sayfaya konmalı, her sayfaya değil. Logo için en az 112x112 piksel, taranabilir bir URL ve beyaz zeminde iyi görünen bir görsel isteniyor.

Bookstore'un örnek verisinde vergi numarası ya da adres ayrıntısı yok; gerçek bir mağazada taxID (Türkiye'de vergi kimlik numarası) ve legalName eklenmeli. Google taxID'nin address'te verilen ülkeyle uyumlu olmasını istiyor.

Bu bölümde resmî belgelerden yararlanılmıştır.

Tek Tanım, Her Yerde Aynı @id

Bookstore'da mağaza düğümü iki biçimde yaşıyor. Her sayfada aynı @id'yle kısa bir OnlineStore düğümü var: ad, URL, logo, sameAs. Tam tanım ise yalnızca "Teslimat ve iade" sayfasında; çünkü Google iade ve kargo politikasını, onu anlatan tek bir sayfada tanımlamayı öneriyor ve markup'ın görünen içerikle aynı olması gerekiyor. O sayfadaki düğümün iade kısmı:

{
  "@type": "OnlineStore",
  "@id": "https://bookstore.example/#organization",
  "name": "Bookstore",
  "url": "https://bookstore.example/",
  "logo": "https://bookstore.example/images/logo-512.png",
  "sameAs": [
    "https://github.com/bookstore-example"
  ],
  "contactPoint": {
    "@type": "ContactPoint",
    "contactType": "customer service",
    "email": "support@example.com"
  },
  "address": {
    "@type": "PostalAddress",
    "addressLocality": "Istanbul",
    "addressCountry": "TR"
  },
  "hasMerchantReturnPolicy": [
    {
      "@type": "MerchantReturnPolicy",
      "@id": "https://bookstore.example/shipping-and-returns#return-policy",
      "applicableCountry": [
        "TR"
      ],
      "returnPolicyCountry": "TR",
      "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
      "merchantReturnDays": 14,
      "returnMethod": "https://schema.org/ReturnByMail",
      "returnFees": "https://schema.org/FreeReturn",
      "refundType": "https://schema.org/FullRefund"
    }
  ]
}

SiteGraph iki biçimi aynı kimlikten üretiyor; tam düğüm, kısa düğümün with ile genişletilmiş hâli:

public string ReturnPolicyId => $"{baseUrl}/shipping-and-returns#return-policy";
public string ShippingServiceId => $"{baseUrl}/shipping-and-returns#standard-shipping";

// Her sayfa aynı kısa mağaza node'unu alır; ayrıntılar yalnız politika sayfasında
public OnlineStoreNode Organization() =>
    new(OrganizationId, "Bookstore", $"{baseUrl}/", $"{baseUrl}/images/logo-512.png",
        ["https://github.com/bookstore-example"]);

Politikanın değerleri tek bir record'da duruyor. Politika sayfasındaki cümleler de JSON-LD de bu nesneden üretiliyor; ücreti değiştirmek tek satırlık bir iş:

using System.Globalization;

namespace Bookstore.Web.Store;

// Mağazanın standart koşulları; politika sayfası da JSON-LD de bu object'ten üretilir
public sealed record StorePolicy(
    string Country,
    int ReturnDays,
    decimal ShippingFee,
    decimal FreeShippingFrom,
    int HandlingDaysMax,
    int TransitDaysMin,
    int TransitDaysMax,
    TimeOnly CutoffTime,
    TimeSpan UtcOffset,
    string Currency = "TRY")
{
    // Örnek politika: teslimattan sonra 14 gün iade, 500 TL'den itibaren ücretsiz kargo
    public static StorePolicy Bookstore { get; } = new(
        Country: "TR",
        ReturnDays: 14,
        ShippingFee: 49.90m,
        FreeShippingFrom: 500.00m,
        HandlingDaysMax: 1,
        TransitDaysMin: 1,
        TransitDaysMax: 3,
        CutoffTime: new TimeOnly(14, 0),
        UtcOffset: TimeSpan.FromHours(3));

    // Offset'li ISO 8601 saat, örneğin 14:00:00+03:00
    public string CutoffTimeWithOffset =>
        new DateTimeOffset(DateOnly.MinValue.ToDateTime(CutoffTime), UtcOffset)
            .ToString("HH:mm:sszzz", CultureInfo.InvariantCulture);
}

MerchantReturnPolicy ve 14 Günlük Cayma Hakkı

Google'ın iade politikası dokümanı mağaza düzeyindeki MerchantReturnPolicy için iki seçenek sunuyor: ya applicableCountry ve returnPolicyCategory ile politikayı tarif edersin ya da yalnızca merchantReturnLink ile politika sayfasını gösterirsin. Bookstore birincisini kullanıyor. MerchantReturnFiniteReturnWindow seçildiğinde merchantReturnDays zorunlu ve bu sayı teslim tarihinden itibaren sayılıyor. İade ücreti için üç değer var: ücretsiz (FreeReturn), müşterinin kendi kargoladığı (ReturnFeesCustomerResponsibility) ve satıcının ücret aldığı (ReturnShippingFees, tutar returnShippingFeesAmount ile).

Türkiye'de bu sayının hukuki bir karşılığı var. 27 Kasım 2014 tarihli Resmî Gazete'de yayımlanan Mesafeli Sözleşmeler Yönetmeliği'nin 9. maddesi tüketiciye on dört gün içinde gerekçe göstermeden cayma hakkı veriyor ve mal teslimine ilişkin sözleşmelerde bu süre malın teslim alındığı gün başlıyor. Google'ın "teslim tarihinden itibaren gün sayısı" tanımıyla örtüşüyor. Yönetmeliğin 15. maddesi ise istisnaları sayıyor; bunlar arasında koruyucu ambalajı açılmış basılı kitaplar ve tüketiciye anında teslim edilen gayrimaddi mallar da var. Bu yazı hukuki bir değerlendirme değil; mesele markup'ın sınırı: MerchantReturnPolicy "ambalajı açılmadıysa" gibi koşulları taşıyamaz. Mağaza düzeyindeki markup genel kuralı anlatır, koşullar politika sayfasının metninde kalır. Kuralın gerçekten farklı olduğu bir ürün (örneğin anında indirilen bir e-kitap) olursa Google'ın yolu, o ürünün Offer'ına kendi MerchantReturnPolicy'sini yazmak.

ShippingService: Kargo Politikasını Bir Kez Yazmak

Mağaza düzeyinde kargo markup'ı yeni: Google 12 Kasım 2025'te Merchant shipping policy dokümanını yayımladı ve Organization altında hasShippingService ile ShippingService tanımlamayı desteklemeye başladı. Zorunlu tek property shippingConditions; her koşul bir varış bölgesi, sipariş tutarı aralığı, ağırlık ya da ürün adedi için bir ücret ve bir teslim süresi tarif ediyor. Bookstore'un kodu:

public OnlineStoreNode Store(StorePolicy policy)
{
    string[] weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"];
    var transit = new ServicePeriodNode(new DaysNode(policy.TransitDaysMin, policy.TransitDaysMax), weekdays);
    var region = new DefinedRegionNode(policy.Country);

    var returns = new MerchantReturnPolicyNode(
        Id: ReturnPolicyId,
        ApplicableCountry: [policy.Country],
        ReturnPolicyCountry: policy.Country,
        ReturnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow",
        MerchantReturnDays: policy.ReturnDays,
        ReturnMethod: "https://schema.org/ReturnByMail",
        ReturnFees: "https://schema.org/FreeReturn",
        RefundType: "https://schema.org/FullRefund");

    var shipping = new ShippingServiceNode(
        Id: ShippingServiceId,
        Name: "Standard shipping",
        FulfillmentType: "https://schema.org/FulfillmentTypeDelivery",
        HandlingTime: new ServicePeriodNode(new DaysNode(0, policy.HandlingDaysMax), weekdays,
            CutoffTime: policy.CutoffTimeWithOffset),
        ShippingConditions:
        [
            // eşiğin altında sabit ücret; maxValue ücretsiz kargo tutarının hemen altı
            new(region, new MonetaryAmountNode(policy.Currency, MaxValue: policy.FreeShippingFrom - 0.01m),
                new MonetaryAmountNode(policy.Currency, Value: policy.ShippingFee), transit),
            new(region, new MonetaryAmountNode(policy.Currency, MinValue: policy.FreeShippingFrom),
                new MonetaryAmountNode(policy.Currency, Value: 0m), transit),
        ]);

    return Organization() with
    {
        ContactPoint = new ContactPointNode("customer service", "support@example.com"),
        Address = new PostalAddressNode("Istanbul", policy.Country),
        HasMerchantReturnPolicy = [returns],
        HasShippingService = [shipping],
    };
}

Ürettiği ShippingService'in ilk koşulu (500 TL altı siparişler) şöyle; ikinci koşul aynı yapıda, minValue: 500.00 ve value: 0 ile:

{
  "@type": "ShippingService",
  "@id": "https://bookstore.example/shipping-and-returns#standard-shipping",
  "name": "Standard shipping",
  "fulfillmentType": "https://schema.org/FulfillmentTypeDelivery",
  "handlingTime": {
    "@type": "ServicePeriod",
    "duration": {
      "@type": "QuantitativeValue",
      "minValue": 0,
      "maxValue": 1,
      "unitCode": "DAY"
    },
    "businessDays": [
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday"
    ],
    "cutoffTime": "14:00:00+03:00"
  },
  "shippingConditions": [
    {
      "@type": "ShippingConditions",
      "shippingDestination": {
        "@type": "DefinedRegion",
        "addressCountry": "TR"
      },
      "orderValue": {
        "@type": "MonetaryAmount",
        "currency": "TRY",
        "maxValue": 499.99
      },
      "shippingRate": {
        "@type": "MonetaryAmount",
        "currency": "TRY",
        "value": 49.90
      },
      "transitTime": {
        "@type": "ServicePeriod",
        "duration": {
          "@type": "QuantitativeValue",
          "minValue": 1,
          "maxValue": 3,
          "unitCode": "DAY"
        },
        "businessDays": [
          "Monday",
          "Tuesday",
          "Wednesday",
          "Thursday",
          "Friday"
        ]
      }
    }
  ]
}

Üç ayrıntı. Birden çok koşul aynı ürüne uyuyorsa Google en düşük kargo ücretini, ücretler eşitse en hızlı teslimatı kullanıyor; ikinci koşulun 500 TL ve üzeri siparişlerde birinciyi ezmesi bu kuraldan da çıkıyor, ama aralıkları açıkça yazmak okuyan geliştirici için daha anlaşılır. cutoffTime saat dilimiyle yazılıyor (14:00:00+03:00); bu saatten sonra gelen siparişe Google teslim tahminine bir gün ekliyor. DefinedRegion'da il düzeyinde bölge (addressRegion) Google'da yalnızca ABD, Avustralya ve Japonya için, posta kodu da yalnızca Avustralya, Kanada ve ABD için destekleniyor; Türkiye için bölge ülke düzeyinde kalıyor.

Offer'dan Politikaya @id ile Gitmek

Mağaza düzeyinde tanımlanan politikalar varsayılan olarak bütün ürünlere uygulanıyor. Google, başka bir sayfadaki genel politikaya bir Offer'dan belirsizlik bırakmadan bağlanmak için @id kullanmayı da belgeliyor: iade için doğrudan hasMerchantReturnPolicy, kargo için shippingDetails içinde hasShippingService. Bookstore bunu kullanıyor:

// mağaza geneli politikalar geçerli; teklif yalnızca onlara işaret eder
HasMerchantReturnPolicy: new IdRef(ReturnPolicyId),
ShippingDetails: new OfferShippingDetailsNode(new IdRef(ShippingServiceId)));
{
  "hasMerchantReturnPolicy": {
    "@id": "https://bookstore.example/shipping-and-returns#return-policy"
  },
  "shippingDetails": {
    "@type": "OfferShippingDetails",
    "hasShippingService": {
      "@id": "https://bookstore.example/shipping-and-returns#standard-shipping"
    }
  }
}

Baştaki senaryonun cevabı burada. Kargo ücreti binlerce Offer'a ayrı ayrı yazılmıyor; değer tek bir yerde, StorePolicy'de. Ürün düzeyinde kendi shippingDetails'ini yazmak yalnızca istisna için var ve Google ürün düzeyi politikaların mağaza düzeyindekilerin bir alt kümesini desteklediğini hatırlatıyor.

Öncelik Sırası: Google Hangi Kaynağa İnanır?

Kargo ve iade bilgisi Google'a birden çok yoldan gidebiliyor ve merchant listing dokümanı bir öncelik sırası veriyor:

08-policy-precedence-tr.png

Sıra, en güçlüden en zayıfa: Merchant Center'a gönderilen ürün düzeyindeki feed'ler, Content API for Shopping ayarları, Merchant Center ya da Search Console'daki ayarlar, ürün düzeyindeki merchant listing markup'ı ve en sonda Organization düzeyindeki markup. Ayrı iade ve kargo politikası sayfalarındaki listede ilk madde yok, dört seviye var; Google'ın verdiği örnek ise net: sitende iade markup'ı varken Search Console'a iade ayarı girersen Google yalnızca Search Console'daki bilgiyi kullanır. Ürün ve mağaza düzeyinde iki farklı iade markup'ı varsa ürün düzeyindeki geçerli.

Baştaki ikinci senaryo bu yüzden tehlikeli: Gül'ün girdiği ayar markup'la aynı değeri söylüyorsa sorun yok, ama bir gün biri yalnızca markup'ı güncellerse Google eski ayarı kullanmaya devam eder. Kural basit: hangi kaynağı kullanacağına karar ver ve politika değişikliğini önce oraya yaz. Bookstore Merchant Center kullanmıyor; tek kaynak markup ve markup'ın tek kaynağı StorePolicy.

Bu bölümde resmî belgelerden yararlanılmıştır.

Testler ve Testin Bulduğu Eski Bir Hata

Yeni test sınıfı üç şeyi kontrol ediyor: politika sayfasındaki markup görünen metinle aynı mı (14 gün, 49,90 TL), politika sayfası dışındaki her sayfada mağaza düğümü kısa mı ve kitap sayfasındaki @id referansları politika sayfasında tanımlanan politikalara mı işaret ediyor. Sonuncusu sayfalar arası bir kontrol, çünkü referans başka bir sayfadaki düğüme gidiyor:

{
    HttpClient client = factory.CreateClient();
    JsonLdPage policyPage = await JsonLdPage.LoadAsync(client, "/shipping-and-returns");
    JsonLdPage bookPage = await JsonLdPage.LoadAsync(client, "/books/dune-paperback");

    JsonElement store = StoreNode(policyPage);
    JsonElement offer = bookPage.Single("Product").GetProperty("offers");

    // @id referansları sayfalar arası; test onları politika sayfasına karşı çözer
    Assert.Equal(
        store.GetProperty("hasMerchantReturnPolicy")[0].GetProperty("@id").GetString(),
        offer.GetProperty("hasMerchantReturnPolicy").GetProperty("@id").GetString());
    Assert.Equal(
        store.GetProperty("hasShippingService")[0].GetProperty("@id").GetString(),
        offer.GetProperty("shippingDetails").GetProperty("hasShippingService").GetProperty("@id").GetString());
}

İlk koşu bir test düşürdü:

failed Bookstore.Web.Tests.StorePolicyTests.Other_pages_carry_only_the_short_store_node(path: "/authors/ada-kaplan") (334ms)
  Assert.Single() Failure: The collection did not contain any matching items
Test run summary: Failed!
  total: 28
  failed: 1
  succeeded: 23
  skipped: 4

Yazar sayfası, üçüncü yazıdan beri Person.worksFor ile mağazanın @id'sine ve ProfilePage.isPartOf ile site düğümüne referans veriyordu, ama ikisini de aynı sayfada tanımlamıyordu. Beşinci yazıdaki dangling @id testi yalnızca yazının author referanslarına bakıyordu; "her sayfada kısa mağaza düğümü" kuralını test edince boşluk ortaya çıktı. Düzeltme Profile()'ın döndürdüğü graph'a Organization() ve WebSite() eklemekti:

$ dotnet test --project Bookstore.Web.Tests
Test run summary: Passed!
  total: 28
  failed: 0
  succeeded: 24
  skipped: 4

Üretimde Dikkat

Politika değişikliğini tek yerden yap ve cache süresini hesaba kat. Kısa mağaza düğümü her sayfada olsa da politika yalnızca bir sayfada; kitap sayfalarının cache'te kalması artık eski bir ücret yayımlamaz, çünkü onlar ücreti değil @id'yi taşıyor.

Search Console ya da Merchant Center'a mağaza ayarı girmeden önce ekiple konuş. Bu ayarlar markup'ın önüne geçiyor ve Search Console'daki hesap düzeyi kargo ve iade ayarlarının Merchant Center'a otomatik eklendiğini Google'ın kendisi yazıyor.

Sadakat programın varsa (MemberProgram, validForMemberTier) aynı yerde tanımlanıyor, ama Google üye kargo avantajı tanımlıyorsan en az bir normal (üye olmayan) kargo hizmeti de vermeni şart koşuyor. Bookstore'da şimdilik böyle bir program yok.

Sık Düşülen Tuzaklar

Aynı politikayı her ürün sayfasında farklı değerlerle tekrar etmek. Bir yerde 14, başka yerde 15 gün yazan bir site, Google'a hangisinin doğru olduğunu söylemiyor; ürün düzeyindeki markup üstte olduğu için hatalı olan kazanabilir.

Search Console'daki mağaza ayarıyla markup'ın çelişmesi. Search Console her zaman kazanır; markup'ı güncellemek tek başına yetmez.

Koşullu istisnaları genel kurala çevirmek. "Ambalajı açılan kitap iade edilmez" diye bütün mağazaya MerchantReturnNotPermitted yazmak, iade hakkı olan bütün siparişler için yanlış bilgi verir.

İl düzeyinde kargo bölgesi tanımlamak. addressRegion Türkiye için desteklenmiyor; İstanbul içi ve dışı için farklı ücret varsa bunu bölgeyle ifade edemezsin. Google'ın shippingRate için desteklediği maxValue ile üst sınırı vermek bu durumda doğru olan.

Mağaza puanını ya da yorumunu OnlineStore'a eklemek. Önceki yazıdaki self-serving kuralı bu düğüm için de geçerli.

Küçük Bir Deneme

★☆☆ StorePolicy.ShippingFee'yi 59,90 olarak değiştir, testleri çalıştır ve kaç dosyaya dokunduğunu say.

★★☆ 2,5 kilogramın üzerindeki siparişler için ayrı bir ücret ekle: ShippingConditionsNode'a weight alanını (QuantitativeValue, unitCode KGM) ekle ve politika sayfasına bunu anlatan bir cümle yaz. Test bu yeni cümleyi nasıl doğrulamalı?

★★★ Bookstore'a anında indirilen e-kitaplar için bir ürün tipi ekle ve yalnızca onların Offer'ına MerchantReturnNotPermitted içeren bir politika yaz. Mağaza düzeyindeki politika diğer kitaplar için aynı kalmalı; bunu bir testle göster.

Bookstore'un kitap sayfası, yorumları ve mağaza bilgisi artık Google'ın mağaza deneyimlerinin istediği yapıda. Serinin son yazısında galerinin geri kalanına bakıyoruz: Event, Discussion forum, Q&A, Image metadata ve diğerleri için kısa tarif kartları ve Bookstore'a hangisinin uymadığı.