Mert Özen Her Satırda Daha İleri
SEO ve GEO Teknik SEO — #14

Video SEO: Watch Page, Video Sitemap, VideoObject, Clip ve SeekToAction ile Key Moments, Thumbnail Kuralları

Mert Özen 15 Kas 2026 12 dk 4 görüntülenme
Video SEO: Watch Page, Video Sitemap, VideoObject, Clip ve SeekToAction ile Key Moments, Thumbnail Kuralları

Video özellikleri için her videonun tek bir watch page'i olmalı; video ilk HTML'de, tıklama beklemeden yüklenmeli ve sitemap, VideoObject ve sayfa aynı thumbnail, başlık ve süreyi söylemeli. Bookstore'un video sayfalarında video sitemap'i, Clip ve SeekToAction ile key moments'ı, ?t= parametresini, sabit thumbnail URL'lerini ve süreli, bölgesel videoları ASP.NET Core ile kurup testle sabitliyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (ASP.NET Core 10.0.15) ile derlenip denenmiştir; kurallar Google'ın video SEO rehberinin (son güncelleme 18 Aralık 2025), video sitemap sayfasının (20 Mayıs 2026) ve video structured data dokümanının (24 Eylül 2026) Kasım 2026'da okunan hâli esas alınarak anlatılmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) bir "Dune Okuma Rehberi" videosu çekti. Video üç yerde duruyor: Dune'un kitap sayfasında, bütün videoları listeleyen /videos sayfasında ve kendi sayfası /videos/dune-reading-guide'da. Pazarlama ekibinin iki isteği var: video üç sayfada da VideoObject ile işaretlensin ki "Google hangisini isterse onu seçsin", ve player sayfa açılışını yavaşlatmasın diye video yalnız bir önizleme görseline tıklanınca yüklensin.

İki soru. Google bu videoyu hangi sayfayla ilişkilendirir? Ve tıklamayla yüklenen bir player'da Google videoyu görür mü?

Bir önceki yazıda görsellerin tek ve sabit bir URL'le, sayfada, sitemap'te ve structured data'da aynı bilgiyle verilmesi gerektiğini gördük. Video aynı ilkeye bir katman ekliyor: bir videonun üç URL'i var ve her birinin Google için ayrı bir anlamı var.

Watch Page: Videonun Asıl İçerik Olduğu Sayfa

Google'ın video rehberinin merkezinde watch page kavramı duruyor: ana amacı kullanıcıya tek bir videoyu izletmek olan sayfa. Bir videonun kendi sayfası, bir dizi bölümünün player sayfası ya da bir maç özetinin sayfası watch page. Videoyu inceleyen bir blog yazısı, 360 derecelik videolu bir ürün sayfası, birçok videoyu listeleyen bir kategori sayfası ya da fragman gömülü bir film eleştirisi değil; oralarda video, sayfanın asıl içeriğini tamamlıyor.

Rehber video sonuçları, video modu, key moments ve LIVE rozeti gibi özelliklere aday olmak için her video için, işin mantığına uyuyorsa, ayrı bir watch page önerirken şartları da sayıyor: watch page index'lenmiş ve aramada iyi performans gösteriyor olmalı, video sayfaya gömülü ve başka element'lerin arkasında gizli olmamalı, geçerli ve sabit URL'li bir thumbnail'ı olmalı. Sayfanın index'lenmesi videonun da index'leneceği anlamına gelmiyor.

Aynı videonun watch page dışında başka sayfalarda da durması sorun değil; rehbere göre böyle bir sayfa metin sonucu ya da video rozetli görsel sonucu olarak görünmeye devam edebiliyor. Bookstore'da karar şu: VideoObject yalnız watch page'de. Kitap sayfası ve video listesi videonun thumbnail'ını gösterip watch page'e link veriyor. Bir test bunu sabitliyor:

[Theory]
[InlineData("/books/dune")]
[InlineData("/videos")]
public async Task Pages_that_only_show_or_list_a_video_are_not_marked_as_watch_pages(string path)
{
    IDocument page = await PageAsync(path);
    Assert.Null(VideoObject(page));
    Assert.Contains(page.QuerySelectorAll("a"), a => a.GetAttribute("href")!.StartsWith("/videos/"));
}

Açılıştaki ilk sorunun cevabı da burada: üç sayfayı birden işaretlemek Google'a seçim bırakmak değil, "bu üç sayfanın üçü de bu videonun sayfası" demek. Doğru sinyal, videonun asıl sayfasını tek ve net göstermek. Markup'ın JSON-LD mekaniği Structured Data ve Schema.org serisinin ikinci ve dördüncü yazılarında; bu yazı videonun bütününe bakıyor.

Google Videoyu Nasıl Bulur?

Rehber videonun <video>, <embed>, <iframe> ya da <object> ile sayfaya gömülmesini istiyor; videoyu URL fragment'iyle (#) yüklemek desteklenmiyor. Açılıştaki ikinci istek ise rehberin açıkça yasakladığı şey: videonun yüklenmesi kaydırma, tıklama ya da yazma gibi bir kullanıcı hareketine bağlanmamalı. "Tıklayınca player'ı yükle" deseni (facade) performans için yaygın, ama Google tıklamadığı için o sayfada bir video görmez. Video JavaScript ile ekleniyorsa, URL Inspection'daki render edilmiş HTML'de göründüğünden emin olmak gerekiyor; bir media API'si kullanılıyorsa API çağrısı başarısız olsa bile video container'ı HTML'de kalmalı.

Bookstore'un watch page'inde video ilk HTML'de, lazy olmadan duruyor. Kendi sunucumuzdaki dosyalar için <video>, YouTube videosu için <iframe>:

@if (video.ContentUrl is not null)
{
    <video controls preload="metadata" poster="@Model.ThumbnailUrl" width="1280" height="720">
        <source src="@(start.Length > 0 ? $"{video.ContentUrl}#t={start}" : video.ContentUrl)" type="video/mp4">
    </video>
}
else
{
    <iframe src="@(start.Length > 0 ? $"{video.PlayerUrl}?start={start}" : video.PlayerUrl)" title="@video.Title"
            width="1280" height="720" allow="encrypted-media; picture-in-picture" allowfullscreen></iframe>
}

start, sayfa URL'indeki ?t= parametresinden geliyor; key moments bölümünde bunun neden gerektiğine döneceğiz.

Üç URL: Watch Page, Player ve Dosya

Rehber bir videoyla ilgili üç URL'i ayrı ayrı tanımlıyor ve her biri sitemap'te ve structured data'da başka bir alana gidiyor:

URLVideo sitemapVideoObjectBookstore örneği
Watch page<loc>sayfanın kendisihttps://bookstore.example/videos/dune-reading-guide
Player<video:player_loc>embedUrlhttps://www.youtube-nocookie.com/embed/bkFoundat12
Video dosyası<video:content_loc>contentUrlhttps://media.bookstore.example/videos/dune-reading-guide.mp4

Google dosyanın kendisini (content URL) tercih ediyor: rehber bunu video dosyasını almanın en etkili yolu olarak tanımlıyor, player URL'i yalnız dosya verilemediğinde yedek. YouTube veya Vimeo gibi bir platformdan gömülen videolarda elinde dosya olmadığı için player URL'i kullanılıyor. Rehbere göre bu durumda Google videoyu hem senin sayfanda hem de platformun kendi sayfasında index'leyebilir; senin sayfan için structured data vermek yine öneriliyor.

Video Sitemap

Video sitemap'in zorunlu alanları <video:thumbnail_loc>, <video:title>, <video:description> ve <video:content_loc> ile <video:player_loc>'tan en az biri. Açıklama en fazla 2048 karakter; iki URL de <loc>'tan, yani watch page'den farklı olmalı. İsteğe bağlı alanlardan süre 1 ile 28800 saniye arasında bir sayı. Mayıs 2022'de <video:category>, <video:gallery_loc>, <video:price>, <video:tvshow> ve player_loc'un autoplay ve allow_embed attribute'ları dokümandan kaldırıldı. Bookstore'un yazıcısı serinin dördüncü yazısındaki sitemap yazıcısının bir kardeşi:

await xml.WriteStartElementAsync("video", "video", VideoNs);
await Element(xml, "thumbnail_loc", site.AbsoluteImageBase + video.ThumbnailPath);
await Element(xml, "title", video.Title);
await Element(xml, "description", video.Description);
// content_loc tercih edilir; yalnız player varsa player_loc
if (video.ContentUrl is not null)
    await Element(xml, "content_loc", video.ContentUrl);
if (video.PlayerUrl is not null)
    await Element(xml, "player_loc", video.PlayerUrl);
await Element(xml, "duration", video.DurationSeconds.ToString());
await Element(xml, "publication_date", video.UploadDate.ToString("yyyy-MM-ddTHH:mm:sszzz"));
$ curl -s http://127.0.0.1:52213/sitemaps/videos.xml | xmllint --format - | head -14
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns:video="http://www.google.com/schemas/sitemap-video/1.1" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://bookstore.example/videos/dune-reading-guide</loc>
    <video:video>
      <video:thumbnail_loc>https://cdn.bookstore.example/images/videos/dune-reading-guide-1280x720.jpg</video:thumbnail_loc>
      <video:title>Dune Reading Guide: Where to Start and What to Watch For</video:title>
      <video:description>A spoiler-free guide to Frank Herbert's Dune: the setting, the main houses and which edition to pick.</video:description>
      <video:content_loc>https://media.bookstore.example/videos/dune-reading-guide.mp4</video:content_loc>
      <video:duration>754</video:duration>
      <video:publication_date>2026-10-12T09:00:00+03:00</video:publication_date>
    </video:video>
  </url>
  <url>

Videonun Türkçe ve İngilizce watch page'i (hreflang ile bağlı iki dil sürümü) sitemap'te ayrı <url>'ler; ikisi de aynı video bilgisini taşıyor.

VideoObject: Sitemap ve Sayfayla Aynı Bilgi

Structured data tarafında VideoObject'in zorunlu alanları name, thumbnailUrl ve uploadDate. Rehber uploadDate'e saat dilimi eklemeyi öneriyor; verilmezse Googlebot'un saat dilimi varsayılıyor. contentUrl, embedUrl, description ve duration önerilen alanlar; Eylül 2026'da creator (ya da author) da önerilenlere eklendi. Rehberin bu bölümdeki asıl vurgusu tutarlılık: markup gerçek videoyla ve diğer metadata ile uyuşmalı, her videonun name, description ve thumbnailUrl değeri kendine özgü olmalı.

Tutarlılık elle sağlanacak bir şey değil. Bookstore'da sitemap, JSON-LD ve sayfa aynı Video kaydından üretiliyor ve bir test üçünü karşılaştırıyor:

string thumbnail = entry.Element(V + "thumbnail_loc")!.Value;
Assert.Equal(thumbnail, ld.GetProperty("thumbnailUrl").GetString());
Assert.Equal(thumbnail, page.QuerySelector("meta[property='og:image']")!.GetAttribute("content"));
Assert.Equal(entry.Element(V + "title")!.Value, ld.GetProperty("name").GetString());
Assert.Equal(page.QuerySelector("h1")!.TextContent, ld.GetProperty("name").GetString());

int seconds = int.Parse(entry.Element(V + "duration")!.Value);
Assert.Equal(seconds, (int)XmlConvert.ToTimeSpan(ld.GetProperty("duration").GetString()!).TotalSeconds);
Assert.InRange(seconds, 1, 28_800);
Assert.True(entry.Element(V + "description")!.Value.Length <= 2048);

Süre sitemap'te saniye (754), JSON-LD'de ISO 8601 (uluslararası tarih ve süre gösterimi standardı) süresi (PT12M34S); XmlConvert ikisi arasında güvenli bir köprü. Test üç videonun her biri için çalışıyor.

Key Moments: Clip, SeekToAction ve YouTube Açıklaması

Key moments, videoyu bir kitabın bölümleri gibi parçalara ayırıp kullanıcıyı doğrudan ilgili ana götüren özellik. Google bölümleri kendisi bulmaya çalışıyor; ama senin structured data ile ya da YouTube açıklamasıyla verdiğin bölümlere öncelik tanıyor. Üç yol var:

  • Clip: her bölümün başlangıcını, isteğe bağlı bitişini ve etiketini sen veriyorsun. Google Search'ün sunulduğu bütün dillerde destekleniyor.
  • SeekToAction: Google'a URL'inde zamanın nereye yazıldığını söylüyorsun, bölümleri Google buluyor. Sınırlı dilde destekleniyor; listede Türkçe de var (İngilizce, İspanyolca, Portekizce, İtalyanca, Çince, Fransızca, Japonca, Almanca, Türkçe, Korece, Felemenkçe ve Rusça). Google'ın video dosyasını alabilmesi şart.
  • YouTube'da barındırılan video: zaman damgalarını ve etiketleri YouTube açıklamasına yazıyorsun; her satırda bir zaman damgası, aynı satırda etiketi, kronolojik sırada.

Clip ve SeekToAction için ortak şartlar: video, başlangıç dışındaki bir noktaya derin link verebilmeli (?t=30 gibi), VideoObject izlenebildiği sayfada olmalı ve video en az 30 saniye sürmeli. Clip'te aynı sayfadaki iki bölüm aynı başlangıcı paylaşamaz; her Clip'in url'i, videonun URL'iyle aynı path'e zamanı belirten bir query parametresi eklenmiş hâli olmalı. Bookstore üçünü de kullanıyor: bölümleri belli olan Dune rehberi için Clip, bölümsüz ama kendi sunucumuzdaki kampanya videosu için SeekToAction, YouTube'daki video için açıklamadaki zaman damgaları:

if (video.Chapters is { Length: > 0 } chapters)
{
    // key moments'ı biz belirliyoruz: her bölüm bir Clip, url aynı path + ?t=
    node["hasPart"] = chapters.Select((c, i) => new Dictionary<string, object>
    {
        ["@type"] = "Clip",
        ["name"] = c.Label,
        ["startOffset"] = c.StartSeconds,
        ["endOffset"] = i + 1 < chapters.Length ? chapters[i + 1].StartSeconds : video.DurationSeconds,
        ["url"] = $"{watchUrl}?t={c.StartSeconds}",
    }).ToArray();
}
else if (video.ContentUrl is not null)
{
    // bölüm yoksa: Google key moments'ı kendisi bulsun, URL'de zamanın nereye yazıldığını söyle
    node["potentialAction"] = new Dictionary<string, object>
    {
        ["@type"] = "SeekToAction",
        ["target"] = $"{watchUrl}?t={{seek_to_second_number}}",
        ["startOffset-input"] = "required name=seek_to_second_number",
    };
}
// YouTube videosunda bölümler YouTube açıklamasındaki zaman damgalarından gelir

Dune rehberinin sayfasından çıkan markup'ın bölümler kısmı:

$ curl -s http://127.0.0.1:52213/videos/dune-reading-guide | grep -o '<script type="application/ld+json">.*</script>' | sed -E 's/<[^>]+>//g' | jq '.hasPart[1:3]'
[
  {
    "@type": "Clip",
    "name": "Arrakis and the spice",
    "startOffset": 95,
    "endOffset": 310,
    "url": "https://bookstore.example/videos/dune-reading-guide?t=95"
  },
  {
    "@type": "Clip",
    "name": "The great houses",
    "startOffset": 310,
    "endOffset": 600,
    "url": "https://bookstore.example/videos/dune-reading-guide?t=310"
  }
]

Bu URL'lerin çalışması gerekiyor: ?t=310 isteği aynı sayfayı açmalı, video 310. saniyeden başlamalı ve sayfa yeni bir URL gibi görünmemeli. Watch page t'yi okuyup <source>'a medya fragment'i (#t=310) olarak, YouTube player'ına start parametresi olarak veriyor; canonical ise her zaman parametresiz:

$ curl -s "http://127.0.0.1:52213/videos/dune-reading-guide?t=310" | grep -E "canonical|<source"
        <link rel="canonical" href="https://bookstore.example/videos/dune-reading-guide">
            <source src="https://media.bookstore.example/videos/dune-reading-guide.mp4#t=310" type="video/mp4">

Bölümler sayfada da <a href="?t=95">1:35 Arrakis and the spice</a> biçiminde link olarak listeleniyor; kullanıcının gördüğü bölümlerle markup'taki bölümler aynı listeden geliyor. Key moments'tan tamamen çıkmak isteyen için yol nosnippet.

Thumbnail Kuralları

Thumbnail'ın şartları rehberde tek tabloda: BMP, GIF, JPEG, PNG, WebP, SVG ya da AVIF; en az 60x30 piksel, daha büyüğü tercih ediliyor; Googlebot'un ve Google'ın görsel crawler'ının erişebileceği, robots.txt ya da login ile engellenmemiş bir adres; piksellerin en az yüzde 80'inin alfa (saydamlık) değeri 250'den büyük, yani neredeyse opak. Thumbnail poster attribute'u, <video:thumbnail_loc>, thumbnailUrl ya da og:video:image ile verilebiliyor ve rehber hepsinde aynı URL'i kullanmayı istiyor.

URL'in sabitliği ayrıca vurgulanıyor. Bazı CDN'ler (statik dosyaları kullanıcıya yakın sunuculardan dağıtan ağlar) kısa sürede geçersiz olan imzalı URL'ler üretiyor; thumbnail URL'i sık değişirse Google videoyu index'leyemeyebiliyor. Key moments ve video önizlemeleri için video dosyasının da sabit bir URL'de olması gerekiyor. Dosyayı herkese açmak istemeyenler için rehberin önerisi, Googlebot'u doğrulayıp contentUrl'i yalnız güvenilen istemcilere vermek (Googlebot doğrulaması serinin on birinci yazısında). Bookstore'da thumbnail yolu videonun slug'ından türüyor ve hiçbir parametre taşımıyor:

// her video için tek ve sabit thumbnail URL'i: deploy'dan deploy'a değişmez
public string ThumbnailPath => $"/images/videos/{Slug}-1280x720.jpg";

Süreli ve Bölgesel Videolar, Canlı Yayın

Bookstore'un "Okula Dönüş 2026" kampanya videosu yıl sonunda yayından kalkacak ve kampanya yalnız Türkiye'de geçerli. Bu iki bilgi hem JSON-LD'de (expires, regionsAllowed) hem sitemap'te (<video:expiration_date>, <video:restriction>) aynı kayıttan üretiliyor:

$ curl -s http://127.0.0.1:52213/sitemaps/videos.xml | xmllint --format - | grep -B2 -A1 restriction | head -4
      <video:publication_date>2026-08-20T10:00:00+03:00</video:publication_date>
      <video:expiration_date>2026-12-31</video:expiration_date>
      <video:restriction relationship="allow">TR</video:restriction>
    </video:video>

Rehberin uyarısı önemli: geçmiş bir tarih videoyu video sonuçlarından çıkarır, yayında kalan bir videoya son kullanma tarihi verme. regionsAllowed hiç verilmezse video bütün bölgeler için uygun sayılıyor; kısıtlama yalnız arama sonuçlarını etkiliyor, videoya erişimi değil. Kampanya videosu bölümsüz ve kendi sunucumuzda olduğu için SeekToAction taşıyor.

Canlı yayın için ayrı bir parça var: VideoObject'e eklenen BroadcastEvent (isLiveBroadcast, startDate, endDate), yayın sırasında sonuçlarda LIVE rozetini mümkün kılıyor. Rehber canlı yayınlarda Indexing API'nin kullanılmasını öneriyor: yayın başladığında, bittiğinde ve markup değiştiğinde. Indexing API'nin videoda desteklediği tek tür canlı yayın.

mertozen.com'un Video Sayfaları

Bu sitenin videoları YouTube'da; her video için bir detay sayfası (/videolar/{slug}) var. Kaynak koddaki hâline göre, bu yazının kontrol listesiyle:

  • Detay sayfası doğru bir watch page: YouTube player'ı <iframe> ile ilk HTML'de ve loading="eager"; tıklamayla yüklenen bir facade yok.
  • VideoObject embedUrl olarak YouTube embed adresini, thumbnail olarak YouTube'un hqdefault.jpg görselini veriyor; bu adres video başına sabit. og:image de aynı görsel.
  • /videolar liste sayfası da sayfadaki her video için bir VideoObject basıyor; Structured Data serisinin dördüncü yazısında da not edildiği gibi liste sayfası bir watch page değil.
  • Videonun yayın tarihi CMS'te (içerik yönetim sistemi) boşsa uploadDate için DateTime.UtcNow kullanılıyor; bu, her istekte değişen bir yükleme tarihi demek. Tarih yoksa alanı hiç üretmemek ya da sabit bir değer kullanmak daha doğru.
  • Sitemap video sayfalarını listeliyor ama video sitemap alanları (<video:video>) yok.
  • Key moments için tek kaynak YouTube açıklamalarındaki zaman damgaları; site ?t= parametresini desteklemediği için Clip ya da SeekToAction şu an uygun değil.

Bunlar sitenin kaynak kodundan çıkan gözlemler; neyin değişeceği site sahibinin kararı.

Üretimde Dikkat

Watch page ile video arasında bire bir ilişki kur. Her videonun tek bir asıl sayfası olsun, bu sayfanın title ve description'ı o videoya özgü olsun. Bir videonun iki dil sürümü varsa bunlar hreflang ile bağlı iki watch page'dir, iki rakip sayfa değil.

Sitemap, JSON-LD, poster ve og:image'i aynı veri kaydından üret ve testle karşılaştır. Thumbnail URL'i, başlık ve süre bir yerde değişip diğerinde değişmediğinde Google'a çelişkili bilgi gider.

Video dosyalarını sunan sunucunun crawl'a dayanabildiğini kontrol et. Rehber hem watch page'in host'unun hem de streaming sunucusunun crawl için yeterli kapasitesi olmasını istiyor; M3U8 (HLS yayınlarının oynatma listesi dosyası) gibi streaming dosyalarının robots.txt ya da noindex ile engellenmemesi gerekiyor.

Bir videoyu hızla kaldırman gerekiyorsa yol Search Console'daki kaldırma isteği; watch page bu sırada 404, noindex ya da sunucu tarafı kimlik doğrulamasıyla Google'a kapalı kalmalı.

Sık Düşülen Tuzaklar

Videoyu tıklamayla yüklemek. Google tıklamaz; o sayfada video yoktur.

Aynı videoyu birçok sayfada VideoObject ile işaretlemek. Asıl sayfa belirsizleşir.

Her istekte değişen ya da süresi dolan imzalı thumbnail URL'leri.

?t= gibi zaman parametreli URL'leri ayrı sayfalar gibi canonical'lamak. Canonical parametresiz watch page olmalı.

Yayındaki bir videoya geçmiş bir expires tarihi vermek.

Clip başlangıçlarını sayfada gösterilen bölümlerden ayrı, elle yazılmış bir listeden üretmek.

Küçük Bir Deneme

★☆☆ Kendi sitende video içeren üç sayfa seç ve her biri için şu soruyu cevapla: bu sayfanın ana amacı tek bir videoyu izletmek mi? VideoObject yalnız cevabı "evet" olan sayfalarda mı?

★★☆ VideoSeoTests'e bir test ekle: bir videonun Clip başlangıçları ile sayfadaki ?t= link'leri aynı liste olsun. Sonra bölümlerden birinin başlangıcını yalnız sayfada değiştirip testin neyi yakaladığına bak.

★★★ Kampanya videosunun Expires tarihi geçtiğinde watch page'in ne yapacağına karar ver ve uygula: video kalkıyorsa sayfa 410 dönsün ve video sitemap'ten düşsün; kalıyorsa expires alanı hiç üretilmesin. İki durumu da testle göster.

Görsel ve video, sayfanın içindeki medyaydı. Bir sonraki yazıda URL sayısını en hızlı büyüten şeye, katalog filtrelerine geçiyoruz: faceted navigation, pagination ve infinite scroll'u crawl budget'ı harcamadan, index'lenmesi gereken filtre sayfalarını doğru kurarak anlatacağız.