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

Product Structured Data: Product Snippet mi Merchant Listing mi? Kitap Sayfası için Product, Offer ve Book

Mert Özen 15 Kas 2026 13 dk 4 görüntülenme
Product Structured Data: Product Snippet mi Merchant Listing mi? Kitap Sayfası için Product, Offer ve Book

Ürün bu sayfadan satın alınabiliyorsa merchant listing kuralları geçerlidir: Offer zorunlu, fiyat sıfırdan büyük; satın alınamıyorsa product snippet. Bookstore'un kitap sayfasında Product ile Book'u birlikte tipliyor, fiyatı ve stoğu görünen sayfayla aynı nesneden üretiyor, kampanya fiyatını ve bitişini işaretliyor ve hepsini xUnit testleriyle sabitliyoruz.

Sürüm notu: Bu yazı Google Search Central'ın Product, Merchant listing, Product snippet ve Product variant 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, AngleSharp 1.10 ve Microsoft.Extensions.TimeProvider.Testing 11.0 ile derlenip denenmiştir.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) Dune'un karton kapaklı baskısını 450 TL'ye satıyor, bu hafta da kampanyayla 360 TL'ye düşürdü. Kitap sayfasının JSON-LD'si (JSON for Linking Data; bağlantılı veriyi JSON ile yazma standardı) ürünün katalogdaki fiyatını, yani 450'yi yazıyor; sayfada ise 360 görünüyor. Ciltli baskı tükenmiş, ama aynı markup kodu onu da InStock diye işaretliyor. Ekipte biri "fiyat ve stok zaten Google'a feed'le gidiyor, markup'taki rakam önemli değil" diyor. Soru: Bu sayfa Google'ın gözünde nasıl bir sayfa ve bu iki hatanın bedeli ne?

Bu yazı serinin mağaza tarafına geçtiği ilk yazı. Önce Google'ın ürün sayfaları için tanımladığı iki deneyimi ayırıyoruz, sonra Bookstore'un kitap sayfasının markup'ını fiyatı ve stoğu görünen sayfayla aynı kaynaktan üretecek şekilde kuruyoruz ve testlerle sabitliyoruz.

İki Deneyim: Product Snippet ve Merchant Listing

Google'ın Product dokümanı ürün markup'ını iki sınıfa ayırıyor ve hangisinin kurallarına uyacağına sayfanın ne olduğuna bakarak karar vermeni istiyor. Product snippet, ziyaretçinin ürünü doğrudan satın alamadığı sayfalar için: editoryal bir inceleme ya da karşılaştırma yazısı. Merchant listing ise ürünü senden satın alabildiği sayfalar için; kargo, iade ve varyant gibi daha ayrıntılı ürün bilgisi taşıyabiliyor.

06-product-or-merchant-tr.png

İki deneyimin zorunlu alanları farklı. Product snippet için name ve review, aggregateRating ya da offers'tan biri yetiyor; teklif olarak Offer da AggregateOffer da kabul ediliyor. Merchant listing ise bir Offer istiyor, çünkü satıcının sen olman gerekiyor: fiyat sıfırdan büyük olmalı, para birimi üç harfli kodla verilmeli, image da zorunlu. Google'ın notuna göre merchant listing'in zorunlu alanlarını eklemek sayfayı genellikle product snippet'e de aday yapıyor. Tersi geçerli değil: başka sitelere link veren bir sayfa merchant listing'e aday olamaz.

İkisinde de ortak bir sınır var: sayfa tek bir ürüne ya da aynı ürünün varyantlarına odaklanmalı. "Bilim kurgu kitapları" gibi bir kategori sayfası belirli bir ürün değil. Bookstore'da bunun anlamı basit: kitap detay sayfası merchant listing'in alanı, blog'da tek bir kitabı inceleyen yazı ise product snippet'in.

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

Kitap Sayfası: Product ve Book Birlikte

Bir kitap hem satılan bir ürün hem de bir yayın. schema.org'da isbn, bookFormat ve numberOfPages gibi property'ler Book tipinde, offers ise Product tarafında. Google'ın merchant listing dokümanı bunu açıkça çözüyor: isbn yalnızca Book üzerinde geçerli, en iyi sonuç için ISBN (International Standard Book Number; kitabın uluslararası standart numarası) 13 haneli biçimiyle verilmeli ve Book, Product ile birlikte tiplenmeli (co-type). Böylece aynı node'da iki tipin property'leri birlikte kullanılabiliyor. Bookstore'un Dune sayfasının EN sürümü şu düğümü üretiyor; TR sayfada yalnızca name, description ve url Türkçe sürüme döner:

{
  "@type": [
    "Product",
    "Book"
  ],
  "@id": "https://bookstore.example/books/dune-paperback#product",
  "name": "Dune (Paperback)",
  "description": "The English paperback edition of the science fiction classic set on the desert planet Arrakis.",
  "image": [
    "https://bookstore.example/images/books/dune-paperback-1x1.jpg",
    "https://bookstore.example/images/books/dune-paperback-4x3.jpg",
    "https://bookstore.example/images/books/dune-paperback-16x9.jpg"
  ],
  "isbn": "9780000000019",
  "sku": "9780000000019",
  "author": {
    "@type": "Person",
    "name": "Frank Herbert"
  },
  "bookFormat": "https://schema.org/Paperback",
  "numberOfPages": 704,
  "inLanguage": "en",
  "offers": {
    "@type": "Offer",
    "url": "https://bookstore.example/en/books/dune-paperback",
    "price": 360.00,
    "priceCurrency": "TRY",
    "availability": "https://schema.org/InStock",
    "itemCondition": "https://schema.org/NewCondition",
    "seller": {
      "@id": "https://bookstore.example/#organization"
    },
    "priceSpecification": {
      "@type": "UnitPriceSpecification",
      "priceType": "https://schema.org/StrikethroughPrice",
      "price": 450.00,
      "priceCurrency": "TRY"
    },
    "validFrom": "2026-11-10T00:00:00+03:00",
    "validThrough": "2026-11-30T23:59:59+03:00"
  }
}

Birkaç ayrıntıya dikkat et. @type bir array; ikinci yazıdaki "en spesifik tip" kuralı burada iki tipe birden ihtiyaç duyulduğu için çoklu tiple çözülüyor. ISBN burada aynı zamanda sku olarak kullanılıyor, çünkü Bookstore'da her baskının stok kaydı ISBN'le tutuluyor; Google sku'nun boşluk içermemesini ve tek değer olmasını istiyor. price bir string değil sayı ve ondalık ayırıcısı nokta; para birimi Türk lirasının kodu TRY. seller, serinin üçüncü yazısındaki Organization düğümüne @id ile bağlanıyor. Görsellerde Google'ın önerdiği üç en-boy oranı (1x1, 4x3, 16x9) var.

Fiyat Tek Yerden Gelmeli

Baştaki hata markup'ın fiyatı katalogdan, sayfanın ise kampanya hesabından okumasıydı. Google'ın genel yönergeleri markup'ın sayfadaki içeriği doğru temsil etmesini istiyor; merchant listing dokümanı da Google'ın ürün verisini göstermeden önce doğrulamaya çalışabileceğini söylüyor. İki ayrı hesap yapan bir kod, bir gün mutlaka farklı iki rakam üretir. Çözüm, fiyatı bir kez hesaplayıp hem sayfaya hem markup'a aynı nesneyi vermek:

using System.Globalization;

namespace Bookstore.Web.Catalog;

// Hem görünen sayfanın hem de JSON-LD'nin okuduğu tek fiyat object'i
public sealed record PriceQuote(
    decimal Price,
    decimal? StrikethroughPrice,
    DateTimeOffset? SaleStartsAt,
    DateTimeOffset? SaleEndsAt,
    string Currency = "TRY");

public sealed class PricingService(BookCatalog catalog, TimeProvider time)
{
    public PriceQuote Quote(BookEdition book)
    {
        DateTimeOffset now = time.GetUtcNow();
        Campaign? campaign = catalog.Campaigns.FirstOrDefault(c =>
            c.Isbn == book.Isbn && c.StartsAt <= now && now <= c.EndsAt);

        return campaign is null
            ? new PriceQuote(book.ListPrice, null, null, null)
            : new PriceQuote(campaign.Price, book.ListPrice, campaign.StartsAt, campaign.EndsAt);
    }

    public static string Format(decimal amount, string lang) => lang == "en"
        ? $"TRY {amount.ToString("N2", CultureInfo.GetCultureInfo("en-US"))}"
        : $"{amount.ToString("N2", CultureInfo.GetCultureInfo("tr-TR"))} TL";
}

PriceQuote sayfanın fiyatla ilgili bilmesi gereken her şeyi taşıyor: geçerli fiyat, varsa üstü çizili eski fiyat ve kampanya aralığı. Saat TimeProvider'dan okunuyor; böylece testte zamanı ileri alıp kampanyanın bittiği anı deneyebiliyoruz. Page model tek bir PriceQuote hesaplıyor ve ikisine de aynı değeri veriyor:

public IActionResult OnGet(string slug)
{
    BookEdition? book = catalog.FindBySlug(slug);
    if (book is null)
    {
        return NotFound();
    }

    Book = book;
    Quote = pricing.Quote(book);   // sayfa ve markup için tek bir PriceQuote
    Lang = Request.Path.StartsWithSegments("/en") ? "en" : "tr";

    ViewData["Lang"] = Lang;
    ViewData["Title"] = $"{book.Title} ({BookTexts.FormatName(book.Format, Lang)})";
    ViewData["JsonLd"] = new SiteGraph("https://bookstore.example").Book(book, Quote, Lang);
    return Page();
}

Görünen fiyat aynı Format method'undan geçiyor:

<p class="price">
    <span class="price-now">@PricingService.Format(quote.Price, lang)</span>
    @if (quote.StrikethroughPrice is { } regular)
    {
        <s class="price-was">@PricingService.Format(regular, lang)</s>
        <span class="sale-ends">@BookTexts.SaleEnds(lang): <time datetime="@quote.SaleEndsAt!.Value.ToString("O")">@quote.SaleEndsAt.Value.ToString("yyyy-MM-dd HH:mm")</time></span>
    }
</p>

Format yalnızca ekrana yazılan metni üretiyor: TR'de 360,00 TL, EN'de TRY 360.00. Markup'taki price ise decimal olarak serialize ediliyor ve System.Text.Json kültürden bağımsız olarak 360.00 yazıyor. Fiyatı string birleştirerek JSON'a koyan bir kod, Türkçe kültürde çalışan bir sunucuda kolayca "360,00" üretir; Google'ın review ve fiyat dokümanları ondalık ayırıcı olarak noktayı istiyor.

Markup'ı Kuran Kod

SiteGraph'a kitap sayfası için bir method ekledik. Node'lar önceki yazılardaki gibi typed record'lar; yeni olan, iki tipli BookProductNode ve Offer:

public sealed record BookProductNode(
    [property: JsonPropertyName("@id"), JsonPropertyOrder(-1)] string Id,
    [property: JsonPropertyName("name")] string Name,
    [property: JsonPropertyName("description")] string Description,
    [property: JsonPropertyName("image")] IReadOnlyList<string> Image,
    [property: JsonPropertyName("isbn")] string Isbn,
    [property: JsonPropertyName("sku")] string Sku,
    [property: JsonPropertyName("author")] NamedNode Author,
    [property: JsonPropertyName("bookFormat")] string BookFormat,
    [property: JsonPropertyName("numberOfPages")] int NumberOfPages,
    [property: JsonPropertyName("inLanguage")] string InLanguage,
    [property: JsonPropertyName("offers")] OfferNode Offers)
{
    [JsonPropertyName("@type"), JsonPropertyOrder(-2)]
    public IReadOnlyList<string> Type => ["Product", "Book"];
}
public JsonLdDocument Book(BookEdition book, PriceQuote quote, string lang)
{
    string url = PageUrl(lang, $"/books/{book.Slug}");
    var offer = new OfferNode(
        Url: url,
        Price: quote.Price,
        PriceCurrency: quote.Currency,
        Availability: book.StockQuantity > 0 ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
        ItemCondition: "https://schema.org/NewCondition",
        Seller: new IdRef(OrganizationId),
        // indirim yalnız kampanya sürerken var: eski fiyat + kampanya aralığı
        PriceSpecification: quote.StrikethroughPrice is { } regular
            ? new UnitPriceSpecificationNode("https://schema.org/StrikethroughPrice", regular, quote.Currency)
            : null,
        ValidFrom: quote.SaleStartsAt,
        ValidThrough: quote.SaleEndsAt);

    var product = new BookProductNode(
        Id: ProductId(book),
        Name: $"{book.Title} ({BookTexts.FormatName(book.Format, lang)})",
        Description: book.Description.Get(lang),
        Image: [.. book.ImagePaths.Select(path => $"{baseUrl}{path}")],
        Isbn: book.Isbn,
        Sku: book.Isbn,
        Author: new NamedNode("Person", book.AuthorName),
        BookFormat: $"https://schema.org/{book.Format}",
        NumberOfPages: book.NumberOfPages,
        InLanguage: book.InLanguage,
        Offers: offer);

    var breadcrumb = new BreadcrumbListNode($"{url}#breadcrumb",
    [
        new(1, BookTexts.Books(lang), PageUrl(lang, "/books")),
        new(2, BookTexts.CategoryName(book.Category, lang), PageUrl(lang, $"/books/category/{book.Category}")),
        new(3, product.Name, null),
    ]);

    return new([product, breadcrumb, Organization(), WebSite()]);
}

availability doğrudan stok miktarından türüyor; ciltli baskının stoğu sıfır olduğu için onun teklifi şu hâlde:

{
  "@type": "Offer",
  "url": "https://bookstore.example/books/dune-hardcover",
  "price": 890.00,
  "priceCurrency": "TRY",
  "availability": "https://schema.org/OutOfStock",
  "itemCondition": "https://schema.org/NewCondition",
  "seller": {
    "@id": "https://bookstore.example/#organization"
  }
}

Stokta olmayan bir baskıda "Sepete ekle" butonu da görünmüyor. Google availability için on değer tanımlıyor (BackOrder, PreOrder, SoldOut…) ve bir teklifte yalnızca birini kabul ediyor. Bookstore'un stok modeli şimdilik iki durum biliyor; ön sipariş eklendiğinde PreOrder de aynı yerden üretilecek. @id dilden bağımsız (/books/dune-paperback#product), url ise ziyaretçinin bulunduğu dildeki sayfayı gösteriyor; üçüncü yazıdaki Person düğümüyle aynı yaklaşım.

Kampanya Fiyatı ve Bitiş Tarihi

Google indirimli fiyatı ayrı bir property ile değil, iki fiyatın ilişkisiyle anlıyor. Teklifin price'ı geçerli fiyat; ikinci bir UnitPriceSpecification, priceType değeri https://schema.org/StrikethroughPrice olarak eski fiyatı taşıyor. Geçerli fiyata priceType yazılmıyor. Google bir geçiş dönemi boyunca ListPrice değerini de kabul ediyor.

Kampanyanın süresi için Google, 7 Temmuz 2026'da merchant listing dokümanına bir "Sale duration" bölümü ekledi. Başlangıç validFrom, bitiş validThrough ya da priceValidUntil ile veriliyor; ikisinin birlikte verilmesi, başlangıcın bitişten sonra olmaması ve saat dilimiyle tam tarih-saat yazılması öneriliyor. Geçerli fiyat Offer üzerindeyse bu tarihler de Offer'a konuyor. Bookstore'un kampanyası 30 Kasım 23:59'da (+03:00) bitiyor ve markup bunu aynen taşıyor.

Bitişten sonra ne olacağı daha önemli. priceValidUntil geçmiş bir tarihi gösteriyorsa Google listelemenin gösterilmeyebileceğini yazıyor. Bookstore'da bu tarihler PriceQuote'tan geldiği için kampanya bittiği anda hem üstü çizili fiyat hem de tarihler markup'tan düşüyor; sayfa cache'te kalmadıkça eski bir tarih yayımlanmıyor.

Ciltli ve Karton Kapak: Varyant mı, Ayrı Ürün mü?

Dune'un iki baskısı var ve ilk akla gelen, ikisini Google'ın product variant markup'ıyla tek sayfada toplamak: bir ProductGroup, altında hasVariant ile iki Product. Google bu yapıyı Şubat 2024'ten beri destekliyor; her varyantın sku ya da gtin gibi benzersiz bir kimliği, grubun da productGroupID'si olmalı. Ama varyantların neye göre ayrıştığını söyleyen variesBy için Google yalnızca altı property listeliyor: color, size, suggestedAge, suggestedGender, material ve pattern. bookFormat bu listede yok.

Bookstore için iki seçenek var:

  • Her baskı kendi sayfasında, kendi Product + Book düğümüyle: ISBN zaten baskıyı tanımlıyor, her sayfanın tek bir fiyatı ve stoğu var; dezavantajı, baskılar arasında geçişin iki ayrı URL üzerinden olması.
  • Tek sayfada ProductGroup: schema.org açısından geçerli, ziyaretçi için tek sayfa; ama Google'ın desteklediği variesBy değerleriyle ifade edilemiyor; ayrıca Google her varyantın ayrı bir URL'le (query parametresiyle) önceden seçilebilmesini istiyor.

Bookstore birincisini seçiyor: bir baskı, bir ISBN, bir sayfa. Aynı ayrım Google'ın Book actions dokümanında da var: orada "work" eserin kendisi, "edition" ise ISBN'i olan somut baskı.

Book actions'ın kendisi ise markup'la tetiklenen bir özellik değil. Okurun arama sonucundan kitabı satın almasını ya da ödünç almasını sağlayan bu özellik bir veri feed'i ile çalışıyor ve Google'ın notuna göre geniş kitap seçkisi olan sağlayıcılarla sınırlı; katılmak için ilgi formu doldurmak gerekiyor ve bu katılımı garanti etmiyor. Bu serinin konusu değil.

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

Testle Sabitlemek

Beşinci yazıdaki JsonLdPage yardımcısı @type'ı her zaman string varsayıyordu; ["Product", "Book"] ile GetString() exception fırlatır. Küçük bir değişiklik yeterli:

public JsonElement Single(string type) => Assert.Single(Nodes, n => HasType(n, type));

// "@type" bir string ya da ["Product", "Book"] gibi birlikte tiplenen node'larda bir array
public static bool HasType(JsonElement node, string type)
{
    JsonElement value = node.GetProperty("@type");
    return value.ValueKind == JsonValueKind.Array
        ? value.EnumerateArray().Any(t => t.GetString() == type)
        : value.GetString() == type;
}

Testler WebApplicationFactory'ye sahte bir saat veriyor, böylece aynı sayfayı kampanya sürerken ve bittikten sonra çekebiliyor:

private static readonly DateTimeOffset DuringSale = new(2026, 11, 15, 12, 0, 0, TimeSpan.FromHours(3));
private static readonly DateTimeOffset AfterSale = new(2026, 12, 1, 0, 0, 0, TimeSpan.FromHours(3));

private HttpClient ClientAt(DateTimeOffset now) =>
    factory.WithWebHostBuilder(builder => builder.ConfigureServices(services =>
        services.AddSingleton<TimeProvider>(new FakeTimeProvider(now)))).CreateClient();

[Theory]
[InlineData("/books/dune-paperback", "tr")]
[InlineData("/en/books/dune-paperback", "en")]
[InlineData("/books/dune-hardcover", "tr")]
public async Task Product_markup_matches_the_visible_page(string path, string lang)
{
    JsonLdPage page = await JsonLdPage.LoadAsync(ClientAt(DuringSale), path);
    JsonElement product = page.Single("Product");
    JsonElement offer = product.GetProperty("offers");

    Assert.True(JsonLdPage.HasType(product, "Book"), "isbn is only valid when Product is co-typed with Book");
    Assert.Equal(page.Document.QuerySelector("h1")!.TextContent, product.GetProperty("name").GetString());

    string isbn = product.GetProperty("isbn").GetString()!;
    Assert.True(Isbn.IsValid13(isbn), $"bad ISBN-13 check digit: {isbn}");
    Assert.Equal(page.Document.QuerySelector(".isbn span")!.TextContent, isbn);

    // merchant listing için fiyatı sıfırdan büyük bir Offer gerekir
    decimal price = offer.GetProperty("price").GetDecimal();
    Assert.True(price > 0, "merchant listings require price > 0");
    Assert.Equal("TRY", offer.GetProperty("priceCurrency").GetString());
    Assert.Equal(page.Document.QuerySelector(".price-now")!.TextContent, PricingService.Format(price, lang));

    bool inStock = offer.GetProperty("availability").GetString() == "https://schema.org/InStock";
    Assert.Equal(inStock, page.Document.QuerySelector("form button") is not null);
    Assert.True(page.HasNode(offer.GetProperty("seller").GetProperty("@id").GetString()!), "dangling seller @id");
    Assert.All(product.GetProperty("image").EnumerateArray(), image => Assert.StartsWith("https://", image.GetString()));
}

İlk test, her dilde ve her baskıda markup'ın görünen sayfayla tutarlı olduğunu kontrol ediyor: ad h1 ile aynı mı, ISBN'in kontrol hanesi doğru mu ve sayfadakiyle aynı mı, fiyat sıfırdan büyük mü ve ekrandaki metinle aynı mı, stok durumu "Sepete ekle" butonuyla uyumlu mu, seller aynı sayfadaki bir düğüme çözülüyor mu. Diğer iki test kampanyanın açık ve kapalı hâllerini deniyor: kampanya sürerken üstü çizili fiyat geçerli fiyattan büyük olmalı, bittiğinde hem priceSpecification hem validThrough kaybolmalı.

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

Baştaki hatayı geri koyalım: SiteGraph'ta Price: quote.Price yerine Price: book.ListPrice:

failed Bookstore.Web.Tests.BookProductTests.Product_markup_matches_the_visible_page(path: "/books/dune-paperback", lang: "tr") (63ms)
  Assert.Equal() Failure: Strings differ
             ↓ (pos 0)
  Expected: "360,00 TL"
  Actual:   "450,00 TL"
             ↑ (pos 0)
Test run summary: Failed!
  total: 13
  failed: 3
  succeeded: 6
  skipped: 4

Aynı değişiklik EN sayfada da (TRY 360.00 yerine TRY 450.00) ve kampanya testinde düşüyor. Ciltli baskının satırı geçiyor, çünkü onun kampanyası yok; hata yalnızca iki kaynağın ayrıştığı yerde görünür.

Üretimde Dikkat

Product markup'ını ilk HTML'e koy. Google, tüm alışveriş sonuçları için optimizasyon yapan satıcılara markup'ı ilk HTML'de vermeyi öneriyor ve JavaScript ile üretilen Product markup'ının alışveriş taramalarını daha seyrek ve daha az güvenilir yapabileceğini, bunun fiyat ve stok gibi sık değişen bilgide sorun çıkarabileceğini yazıyor. Bookstore'un Razor sayfaları bunu zaten yapıyor. Angular istemcisi bookstore-web'de aynı kural SSR (Server-Side Rendering; sayfanın HTML'ini sunucuda üretmek) ile geçerli: fiyatı template'e veren signal'la JSON-LD'yi üreten service'e veren signal aynı olmalı ve script ikinci yazıdaki gibi server'da bir kez yazılmalı.

Para birimi başına ayrı URL kullan. Google, ürünü birden çok para birimiyle satıyorsan her para birimi için ayrı bir URL istiyor. Bookstore'un EN sayfası da TL ile satıyor; EN sayfada fiyatı başka bir para birimine çevirmek istersen bu ayrı bir URL kararı.

Sayfayı cache'liyorsan süresini kampanya ve stok değişimleriyle uyumlu tut. Markup'ın kendisi doğru olsa bile 10 dakikalık bir output cache, kampanyanın bitişinden sonra 10 dakika boyunca eski fiyatı ve eski validThrough'u yayımlar.

Merchant Center feed'i bu serinin konusu değil, ama ilişkiyi bil: Google hem markup hem feed verildiğinde uygunluğun en geniş olduğunu ve bazı deneyimlerin ikisini birleştirdiğini söylüyor. Feed varsa ikisi aynı fiyatı söylemeli.

Sık Düşülen Tuzaklar

Stokta olmayan ürünü InStock bırakmak. Markup'ı stoktan bağımsız bir sabitle yazmak en sık görülen hata; ziyaretçi sepete ekleyemediği bir ürün için arama sonucunda "stokta" görür.

Sayfada görünmeyen bir fiyatı markup'a koymak. Sepette uygulanan bir kupon indirimi ya da üye fiyatı, sayfada görünmüyorsa geçerli fiyat olarak yazılmaz. Üye fiyatı için Google'ın ayrı bir yapısı var (validForMemberTier).

Varyantlar için AggregateOffer kullanmak. Google, AggregateOffer'ı bir ürünü birden çok satıcının sattığı durumlar için tanımlıyor ve varyant kümesini anlatmak için kullanılmamasını açıkça yazıyor; merchant listing zaten yalnızca Offer kabul ediyor.

isbn'i tek başına Product'a yazmak. schema.org'da isbn, bookFormat ve numberOfPages'in tek domain'i Book; Book tipi olmadan bu property'ler yanlış yerde.

Book actions'ı markup ile açılan bir özellik sanmak. Kitap sayfana ReadAction eklemek bu özelliği açmaz; feed ve Google'ın onayı gerekiyor.

Küçük Bir Deneme

★☆☆ BookCatalog'a stoğu 3 olan üçüncü bir baskı ekle ve "son 3 adet" bilgisini sayfada gösterecek şekilde availability için LimitedAvailability kullan. Testin hangi satırı yeni duruma göre değişmeli?

★★☆ PriceQuote'a bir kampanya başlangıcından önceki durumu ekle: kampanya henüz başlamadıysa ne sayfada ne markup'ta indirim görünmeli. Bunu FakeTimeProvider ile 9 Kasım'a ayarlanmış bir testle göster.

★★★ Dune'un iki baskısını tek sayfada ProductGroup ile işaretleyen ikinci bir sayfa yaz, beşinci yazıdaki gibi Schema Markup Validator ve Rich Results Test'te iki sürümü karşılaştır ve variesBy olmadan Google'ın varyantları nasıl gösterdiğini not et.

Kitap sayfası artık fiyatını ve stoğunu dürüstçe anlatıyor; eksik olan, okurun ne düşündüğü. Sonraki yazıda kitap yorumlarını ve ortalama puanı ele alıyoruz: kimin yorumu sayılır, puan nasıl hesaplanır ve Google'ın self-serving review kuralı Bookstore'u nerede durdurur.