Structured Data'yı Test Etmek ve İzlemek: Rich Results Test, Schema Markup Validator, Search Console ve CI'da Otomatik Kontrol
Rich Results Test Google'ın rich result uygunluğunu, Schema Markup Validator schema.org geçerliliğini kontrol eder; regresyonu ise ancak her build'de çalışan testler yakalar. URL Inspection ve Search Console raporlarını, xUnit ve WebApplicationFactory ile JSON-LD testlerini, Angular SSR için Playwright kontrollerini ve deploy sonrası smoke test'leri gerçek başarılı ve başarısız çıktılarla gösteriyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit v3 4.0, Microsoft.AspNetCore.Mvc.Testing 10.0, AngleSharp 1.10, Angular 22.2 ve Playwright 1.65 ile derlenip denenmiştir; Google araçları ve Search Console raporları Kasım 2026 hâliyle anlatılmıştır.
Önce Tahmin Et
Bookstore'un JSON-LD'si (JSON for Linking Data; bağlantılı veriyi JSON ile yazma standardı) Schema Markup Validator'dan sıfır hatayla geçiyor. Bir ekip arkadaşın "o zaman Google'da da sorun yok" diyor. Bir başkası JsonLd.cs'teki encoder'ı "Türkçe karakterler kaçmasın" diye UnsafeRelaxedJsonEscaping'e çeviren bir pull request açıyor; bütün testler yeşil. Üçüncü bir değişiklik Angular tarafında JSON-LD service'ini sadeleştiriyor ve SSR (Server-Side Rendering; sayfanın HTML'ini sunucuda üretmek) çıktısı curl ile bakıldığında düzgün görünüyor. Soru: Bu üç durumdan hangisi gerçekten güvende?
Hiçbiri. Birincisi yanlış aracı yanlış soruyla kullanıyor; ikinci ve üçüncüsü, structured data'yı test etmeyen bir test suite'inin yeşil kalmasından ibaret. Bu yazı doğru aracı doğru soru için seçmeyi ve bu kontrolleri CI'a koymayı anlatıyor.
İki Araç, İki Soru
Google'ın eski Structured Data Testing Tool'u Aralık 2020'de duyurulan bir planla ikiye ayrıldı: Google'a özgü kontroller Rich Results Test'e kaldı, genel schema.org doğrulaması 2021'de validator.schema.org adresinde Schema Markup Validator olarak schema.org topluluğuna geçti. İki aracın sorduğu soru farklı:
| Soru | Rich Results Test | Schema Markup Validator |
|---|---|---|
| Neyi kontrol eder? | Google'ın desteklediği rich result tiplerine uygunluğu | schema.org'a göre geçerliliği |
| Google'ın tanımadığı bir tip | Görmezden gelir | Ayrıştırır ve gösterir |
| Kaldırılan bir tip (FAQ, HowTo) | Artık göstermez | Geçerli schema.org olarak gösterir |
| Zorunlu property eksikse | Kritik hata, öğe geçersiz | Genellikle sorun yok |
| Girdi | URL veya kod | URL veya kod |
Baştaki ilk iddianın sorunu bu tabloda. Serinin dördüncü yazısındaki mertozen.com araç sayfası, Schema Markup Validator'a göre tamamen geçerli bir WebApplication taşıyor; ama Google'ın Software App rich result'ı için zorunlu olan puan ya da yorum olmadığından Rich Results Test'te bir uygunluk göremezsin. "Geçerli" ile "uygun" ayrı sorular.
İki aracın da URL ve kod modu var. Google'ın JavaScript ile structured data üretme dokümanı, Rich Results Test'te URL modunu öneriyor, çünkü kod modunun CORS (Cross-Origin Resource Sharing; tarayıcının başka origin'lere yapılan istekleri kısıtlaması) gibi JavaScript sınırlamaları var. Schema Markup Validator'ı mertozen.com'un bir yazısında iki şekilde denedik: HTML'i kod olarak verdiğimizde 2 öğe, 0 hata, 0 uyarı; aynı URL'i verdiğimizde yanıt isRendered: true ile yine 2 öğe, 0 hata, 0 uyarı. Ama URL modundaki ilk denememiz fetchError: UNREACHABLE ile döndü ve aynı dakikalarda site kısa bir süre 503 veriyordu. Araç sayfaya ulaşamadığında markup'ın kendisi hakkında hiçbir şey söylemez; "hata yok" ile "bakamadım"ı karıştırma.
Google'ın Gördüğü: URL Inspection
Rich Results Test sayfayı o anda çeker; Google'ın index'indeki sürümü ise Search Console'daki URL Inspection gösterir. Index'lenmiş sürümün Enhancements bölümü bulunan rich result'ları, geçerli öğe sayısını ve uyarı ya da hataları listeler; "View crawled page" ham HTML'i ve yüklenen kaynakları gösterir. "Test live URL" aynı kontrolü canlı sayfada yapar ve bir ekran görüntüsü ekler. Bir not: URL Inspection her rich result tipini göstermiyor; Google'ın yardım sayfası desteklenmeyen tiplerin yine de geçerli olabileceğini ve Search'te görünebileceğini, yalnızca bu araçta listelenmeyeceğini yazıyor. URL Inspection'ın genel kullanımı, SEO (Search Engine Optimization; arama motorları için optimizasyon) konusundaki temel serimiz SEO Temelleri'nin Search Console yazısında.
Bu iki görünüm arasındaki fark, "test aracında geçiyor ama canlıda yok" türü sorunların ilk ipucu. Index'lenmiş sürümde markup yoksa ve canlı testte varsa, Google sayfanın yeni hâlini henüz işlememiştir; ikisinde de yoksa sorun senin render'ında.
Search Console Raporları
Search Console, geçerli markup'ını bulduğu her desteklenen tip için bir rich result durum raporu açar. Rapor geçerli ve geçersiz öğe sayılarını zaman içinde gösterir: bir öğeyi rich result'tan alıkoyan kritik sorunlar "Why items are invalid" altında, görünümü iyileştirecek kritik olmayan sorunlar "Improve item appearance" altında. Örnek tablosu en fazla 1.000 satır gösterir, yani sayılar sitedeki gerçek öğe sayısından düşük olabilir.
Raporlar Google'ın desteklediği tip listesine bağlı olduğu için bir tip kaldırıldığında rapor da gider. HowTo'nun rapor ve Rich Results Test desteği 2023'te özelliğin kalkmasından 30 gün sonra, Search Console API desteği 180 gün sonra kaldırıldı. 2025 dalgasındaki altı tip 9 Eylül 2025'te raporlardan, Rich Results Test'ten ve arama görünümü filtrelerinden çıkarıldı; API desteği Aralık 2025'te bitti. Practice problem için aynı süreç Ocak 2026'da başladı. Bu raporlara dayanan bir dashboard ya da bulk export sorgusu o tarihlerde sessizce boşalır. Google'ın Eylül 2025 notu, bulk export'ta kaldırılan görünüm alanlarının NULL döneceğini ve sorguların is_learning_videos IS NOT TRUE gibi hem false hem NULL ile çalışan bir biçime çevrilmesini öneriyordu.
xUnit ve WebApplicationFactory ile Sayfayı Test Etmek
Araçlar bir sayfayı bir kez kontrol eder; regresyonu yakalamak için kontrolün her build'de çalışması gerekir. Bookstore'un test projesi WebApplicationFactory ile uygulamayı bellekte ayağa kaldırıyor, sayfayı gerçek bir HTTP isteğiyle çekiyor ve JSON-LD'yi bir HTML parser ile okuyor. .NET 10 SDK'da xUnit v3, dotnet test için Microsoft.Testing.Platform runner'ını açıkça seçmeyi istiyor; aksi hâlde VSTest hedefinin artık desteklenmediğini söyleyen bir hatayla duruyor. Seçim global.json'da:
{
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
İlk parça, bir sayfanın JSON-LD bloklarını tek bir düğüm listesine açan küçük bir yardımcı:
using System.Text.Json;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
namespace Bookstore.Web.Tests;
// Render edilmiş bir sayfanın JSON-LD blokları, düz bir node listesine açılmış hâlde
public sealed class JsonLdPage(string rawHtml, IHtmlDocument document, IReadOnlyList<JsonElement> nodes)
{
public string RawHtml { get; } = rawHtml;
public IHtmlDocument Document { get; } = document;
public IReadOnlyList<JsonElement> Nodes { get; } = nodes;
public static async Task<JsonLdPage> LoadAsync(HttpClient client, string path)
{
string html = await client.GetStringAsync(path, TestContext.Current.CancellationToken);
// regex değil gerçek bir HTML parser: attribute değerleri entity-encoded olabilir
IHtmlDocument document = new HtmlParser().ParseDocument(html);
var nodes = new List<JsonElement>();
foreach (var script in document.QuerySelectorAll("script[type='application/ld+json']"))
{
JsonElement root = JsonDocument.Parse(script.TextContent).RootElement;
Assert.Equal("https://schema.org", root.GetProperty("@context").GetString());
nodes.AddRange(root.TryGetProperty("@graph", out JsonElement graph) ? graph.EnumerateArray() : [root]);
}
return new JsonLdPage(html, document, nodes);
}
public JsonElement Single(string type) => Assert.Single(Nodes, n => n.GetProperty("@type").GetString() == type);
public bool HasNode(string id) => Nodes.Any(n => n.TryGetProperty("@id", out JsonElement x) && x.GetString() == id);
}
Regex yerine AngleSharp kullanmanın sebebi ikinci yazıdaki ld+json örneği: attribute değerleri entity-encoded gelebilir ve bunu doğru okuyan tek şey gerçek bir HTML parser. Testler üç şeyi kontrol ediyor:
using System.Text.Json;
using Bookstore.Web.Content;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
namespace Bookstore.Web.Tests;
public sealed class StructuredDataTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Theory]
[InlineData("/blog/csharp-books-for-beginners")]
[InlineData("/en/blog/csharp-books-for-beginners")]
public async Task BlogPosting_matches_visible_content(string path)
{
JsonLdPage page = await JsonLdPage.LoadAsync(factory.CreateClient(), path);
JsonElement posting = page.Single("BlogPosting");
Assert.Equal(page.Document.QuerySelector("h1")!.TextContent, posting.GetProperty("headline").GetString());
DateTimeOffset published = posting.GetProperty("datePublished").GetDateTimeOffset();
Assert.Matches(@"[+-]\d{2}:\d{2}$|Z$", posting.GetProperty("datePublished").GetString()!);
if (posting.TryGetProperty("dateModified", out JsonElement modified))
{
Assert.True(modified.GetDateTimeOffset() >= published, "dateModified is earlier than datePublished");
}
foreach (JsonElement image in posting.GetProperty("image").EnumerateArray())
{
Assert.StartsWith("https://", image.GetString());
}
// her author referansı aynı sayfadaki bir node'a çözülmeli
foreach (JsonElement author in posting.GetProperty("author").EnumerateArray())
{
Assert.True(page.HasNode(author.GetProperty("@id").GetString()!), "dangling author @id");
}
}
[Fact]
public async Task Person_id_is_the_same_on_post_profile_and_both_languages()
{
HttpClient client = factory.CreateClient();
JsonLdPage trPost = await JsonLdPage.LoadAsync(client, "/blog/csharp-books-for-beginners");
JsonLdPage enPost = await JsonLdPage.LoadAsync(client, "/en/blog/csharp-books-for-beginners");
JsonLdPage profile = await JsonLdPage.LoadAsync(client, "/authors/ada-kaplan");
string? personId = trPost.Single("Person").GetProperty("@id").GetString();
Assert.Equal(personId, enPost.Single("Person").GetProperty("@id").GetString());
Assert.Equal(personId, profile.Single("ProfilePage").GetProperty("mainEntity").GetProperty("@id").GetString());
}
[Fact]
public async Task Hostile_title_cannot_close_the_script_element()
{
const string title = "Why </script><script>alert(1)</script> breaks pages";
var catalog = new PostCatalog();
Post original = catalog.Posts[0];
Post hostile = original with { Slug = "hostile", Title = new Localized(title, title) };
HttpClient client = factory.WithWebHostBuilder(builder => builder.ConfigureServices(services =>
services.AddSingleton(new PostCatalog { Posts = [hostile] }))).CreateClient();
JsonLdPage page = await JsonLdPage.LoadAsync(client, "/blog/hostile");
Assert.DoesNotContain("<script>alert(1)", page.RawHtml);
Assert.Equal(title, page.Single("BlogPosting").GetProperty("headline").GetString());
}
}
Birinci test, markup'ın görünen sayfayla tutarlılığını kontrol ediyor: headline sayfadaki h1 ile aynı mı, tarih saat dilimi taşıyor mu, dateModified yayından sonra mı, görseller mutlak mı ve her author referansı aynı sayfadaki bir düğüme çözülüyor mu. Her iki dil için çalışıyor; üçüncü yazıdaki EN breadcrumb sorunu tam bu tür bir testin işi. İkinci test, @id tutarlılığını sayfalar arasında kontrol ediyor: TR yazıdaki, EN yazıdaki ve yazar sayfasındaki Person kimliği aynı mı? Üçüncü test, başlığında </script> geçen bir yazıyı WithWebHostBuilder ile catalog'a koyup encoder'ı sınıyor. Bunun için PostCatalog'un listeleri init ile ayarlanabilir ve Program.cs'in sonunda public partial class Program; var.
$ dotnet test --project Bookstore.Web.Tests
Test run summary: Passed!
total: 8
failed: 0
succeeded: 4
skipped: 4
Atlanan dört test, aşağıdaki deploy smoke test'leri. Şimdi baştaki pull request'i uygulayalım: JsonLd.cs'te encoder'ı UnsafeRelaxedJsonEscaping yapıp aynı komutu çalıştırınca:
failed Bookstore.Web.Tests.StructuredDataTests.Hostile_title_cannot_close_the_script_element
Xunit.MicrosoftTestingPlatform.XunitException: System.Text.Json.JsonReaderException : Expected end of string, but instead reached end of data. LineNumber: 0 | BytePositionInLine: 137.
Test run summary: Failed!
total: 8
failed: 1
succeeded: 3
skipped: 4
Hata mesajı ne olduğunu doğru anlatıyor: HTML parser script element'ini başlıktaki </script>'te kapattığı için JSON 137. byte'ta yarıda kesildi. Aynı pull request, bu test olmadan yeşil geçerdi.
Angular SSR için Playwright
Angular tarafında iki ayrı şeyi kontrol etmek gerekiyor: sunucunun gönderdiği ilk HTML ve hydration'dan sonraki DOM (Document Object Model; tarayıcının sayfayı temsil ettiği element ağacı). Playwright ikisini de yapabiliyor: request fixture'ı ham HTTP yanıtını, page fixture'ı Chromium'da çalışan sayfayı veriyor.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
reporter: 'list',
use: { baseURL: 'http://localhost:4321' },
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: {
command: 'node dist/bookstore-ssr/server/server.mjs',
url: 'http://localhost:4321/blog/csharp-books-for-beginners',
env: { PORT: '4321', NG_ALLOWED_HOSTS: 'localhost' },
},
});
import { expect, test } from '@playwright/test';
const slugs = ['csharp-books-for-beginners', 'html-in-titles'];
for (const slug of slugs) {
test(`SSR HTML of ${slug} has one parseable BlogPosting`, async ({ request }) => {
const html = await (await request.get(`/blog/${slug}`)).text();
const blocks = [...html.matchAll(/<script type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/g)];
expect(blocks).toHaveLength(1);
const data = JSON.parse(blocks[0][1]);
expect(data['@type']).toBe('BlogPosting');
expect(data.datePublished).toMatch(/[+-]\d{2}:\d{2}$|Z$/);
// JSON-LD içindeki HTML entity'leri escape değil, veri bozulmasıdır
expect(data.headline).not.toMatch(/&(lt|gt|amp);/);
});
test(`${slug} keeps a single JSON-LD block after hydration`, async ({ page }) => {
await page.goto(`/blog/${slug}`);
await page.waitForLoadState('networkidle');
const scripts = page.locator('script[type="application/ld+json"]');
await expect(scripts).toHaveCount(1);
const data = JSON.parse((await scripts.first().textContent()) ?? '');
await expect(page.locator('h1')).toHaveText(data.headline);
});
}
webServer bloğu build edilmiş SSR sunucusunu testten önce başlatıyor. Buradaki regex bilinçli: Angular SSR type attribute'unu encode etmiyor ve bu test tam olarak sunucunun gönderdiği metni denetlemek istiyor. Doğru service ile:
$ npx playwright test
Running 4 tests using 1 worker
✓ 1 [chromium] › e2e/json-ld.spec.ts:6:3 › SSR HTML of csharp-books-for-beginners has one parseable BlogPosting
✓ 2 [chromium] › e2e/json-ld.spec.ts:17:3 › csharp-books-for-beginners keeps a single JSON-LD block after hydration
✓ 3 [chromium] › e2e/json-ld.spec.ts:6:3 › SSR HTML of html-in-titles has one parseable BlogPosting
✓ 4 [chromium] › e2e/json-ld.spec.ts:17:3 › html-in-titles keeps a single JSON-LD block after hydration
4 passed
İkinci yazıdaki ilk, sade service sürümüyle, yani her çağrıda yeni script ekleyen ve JSON'u escape etmeyen hâliyle:
✓ 1 [chromium] › e2e/json-ld.spec.ts:6:3 › SSR HTML of csharp-books-for-beginners has one parseable BlogPosting
✘ 2 [chromium] › e2e/json-ld.spec.ts:17:3 › csharp-books-for-beginners keeps a single JSON-LD block after hydration
✘ 3 [chromium] › e2e/json-ld.spec.ts:6:3 › SSR HTML of html-in-titles has one parseable BlogPosting
✘ 4 [chromium] › e2e/json-ld.spec.ts:17:3 › html-in-titles keeps a single JSON-LD block after hydration
Error: expect(locator).toHaveCount(expected) failed
Expected: 1
Received: 2
Error: expect(received).not.toMatch(expected)
Expected pattern: not /&(lt|gt|amp);/
Received string: "Why </script> in a title breaks pages"
3 failed
1 passed
İki ayrı hata, iki ayrı test. Hydration testleri ikinci script'i yakalıyor. SSR testi ise curl ile bakarken gözden kaçması kolay olan şeyi yakalıyor: Angular'ın server DOM'u < karakterini < olarak yazdığı için başlık bozulmuş. Bu test olmasaydı, sorun yalnızca sayfanın ilk HTML'ini okuyan sistemlerde görünecekti; tarayıcıda hydration değeri düzelttiği için kimse fark etmeyecekti.
Deploy Sonrası Smoke Test
Testten geçen kod canlıda farklı davranabilir: bir CDN (Content Delivery Network; içeriği kullanıcıya yakın sunuculardan dağıtan ağ) eski HTML'i önbellekte tutar, production config'i farklı bir base URL verir ya da SSR sunucusu bir hata yüzünden CSR'a (Client-Side Rendering; sayfayı tarayıcıda JavaScript ile oluşturmak) düşer. Bunun için aynı JsonLdPage yardımcısını gerçek bir adrese yönlendiren küçük bir test sınıfı yeterli:
using System.Text.Json;
namespace Bookstore.Web.Tests;
// Yalnızca SMOKE_BASE_URL set edildiğinde çalışır, örneğin deploy'dan hemen sonra
public sealed class DeployedSmokeTests
{
private static readonly string? BaseUrl = Environment.GetEnvironmentVariable("SMOKE_BASE_URL");
[Theory]
[InlineData("/", "WebSite")]
[InlineData("/blog/csharp-books-for-beginners", "BlogPosting")]
[InlineData("/en/blog/csharp-books-for-beginners", "BlogPosting")]
[InlineData("/authors/ada-kaplan", "ProfilePage")]
public async Task Live_page_serves_expected_type(string path, string expectedType)
{
if (string.IsNullOrEmpty(BaseUrl))
{
Assert.Skip("SMOKE_BASE_URL is not set");
}
using var client = new HttpClient { BaseAddress = new Uri(BaseUrl) };
JsonLdPage page = await JsonLdPage.LoadAsync(client, path);
JsonElement node = page.Single(expectedType);
Assert.StartsWith("https://", node.GetProperty("@id").GetString());
}
}
SMOKE_BASE_URL yoksa testler Assert.Skip ile atlanıyor; bu yüzden normal dotnet test çalıştırmasında dört test "skipped" görünüyordu. Deploy pipeline'ının son adımında ortam değişkeniyle çalıştırıldığında yalnızca bu sınıf koşuyor. Burada yerel uygulamaya karşı:
$ SMOKE_BASE_URL=http://localhost:5321 dotnet test --project Bookstore.Web.Tests -- --filter-class "*DeployedSmokeTests"
Test run summary: Passed!
total: 4
failed: 0
succeeded: 4
skipped: 0
Örnek URL listesini küçük tut: ana sayfa, her dilde bir yazı, bir yazar sayfası. Amaç her sayfayı taramak değil, deploy'un markup'ı toptan bozup bozmadığını dakikalar içinde görmek.
Bakım Rutini
Structured data bir kez yazılıp unutulan bir şey değil. Üç kaynağı düzenli takip et. Birincisi Google'ın Search Central değişiklik günlüğü: bu serideki her tarih oradan geldi ve bir tipin deprecation notu genellikle kaldırılmadan haftalar önce orada görünüyor. İkincisi schema.org sürüm notları: 30.0 Mart 2026'da, 30.1 Eylül 2026'da çıktı ve yeni tipler ya da property anlamı değişiklikleri buradan duyuruluyor. Üçüncüsü Search Console'daki Manual Actions raporu: structured data spam'i için bir manual action, rich result uygunluğunu kaldırır ve burada görünür.
Markup değişikliğinin etkisini ölçmek ayrı bir iş. Search Console'un Performance raporundaki arama görünümü filtresi, rich result olarak görünen gösterimleri ve tıklamaları ayırmayı sağlıyor; ama "markup ekledik, trafik arttı" sonucuna varmadan önce diğer değişkenleri ayıklamak gerekiyor. Bu konu bu turda yazılan "Analytics ve Ölçüm" serisinde ayrıntılı ele alınıyor.
Üretimde Dikkat
Testleri her iki dil ve her sayfa tipi için çalıştır. Bookstore'da TR ve EN aynı kodla üretiliyor, ama mertozen.com örneğinde gördüğümüz gibi dil başına URL kurmak ayrı bir kod yolu ve ayrı bir hata kaynağı.
Rich Results Test'i manuel bir kontrol olarak bırak. Google bu araç için bir API sunmuyor; CI'da Google'ın uygunluk kararını değil, kendi kurallarını test edersin. Google'ın zorunlu property'lerini bu kurallara yazmak, ikisinin arasındaki boşluğu daraltır.
Uyarı ile hatayı ayır. Search Console'da "Improve item appearance" altındaki bir uyarı öğeyi geçersiz yapmaz; "Why items are invalid" altındaki bir hata yapar. Uyarıları sıfırlamak için görünür içerikte olmayan bir property'yi doldurmak, çözdüğünden büyük bir sorun yaratır.
Sık Düşülen Tuzaklar
Test aracında geçen markup'ın canlıda farklı olması. Önbellekteki eski HTML, dile göre farklı render edilen sayfa ya da SSR hatası yüzünden CSR'a düşen route, aracın gördüğü sayfayı ziyaretçinin ve Googlebot'un gördüğünden ayırır. Deploy sonrası smoke test ve URL Inspection'daki index'lenmiş sürüm bu farkı yakalar.
Uyarıları hata sanmak ya da tersi. Bir yandan her uyarıyı kapatmak için uydurma değer yazmak, öte yandan "nasılsa geçiyor" diye kritik bir hatayı ertelemek; ikisi de raporu yanlış okumaktan doğuyor.
Kaldırılan bir tipin raporu kaybolunca sorunun çözüldüğünü sanmak. Rapor tip desteklenmediği için gitti; markup hâlâ sayfada ve hâlâ bakım gerektiriyor.
JSON-LD'yi regex ile ayıklamak. ASP.NET Core 9 ve sonrasında type attribute'u encode edilebiliyor, script'in başka attribute'ları olabiliyor, aynı sayfada birden çok blok olabiliyor. Test kodunda bir HTML parser kullan; Playwright'taki gibi bilinçli olarak ham metni denetlediğin yerde de bunu açıkça not et.
Küçük Bir Deneme
★☆☆ DeployedSmokeTests'e bir URL daha ekle ve testin, o sayfada beklenen tip yoksa hangi mesajla düştüğüne bak.
★★☆ StructuredDataTests'e, sayfadaki breadcrumb'ın her item değerinin sayfanın dil önekiyle başladığını kontrol eden bir test ekle. Bookstore'da geçmeli; mertozen.com'un EN yazısına karşı smoke test olarak çalıştırıldığında düşmeli.
★★★ GitHub Actions ya da Azure DevOps'ta, önce dotnet test'i, ardından Angular build'i ve npx playwright test'i çalıştıran, deploy'dan sonra da SMOKE_BASE_URL ile smoke test'leri koşturan bir pipeline yaz.
Seri burada bitiyor. Structured data'nın ne olduğundan başladık, JSON-LD'yi güvenle üretmeyi, bir teknik blog için entity graph kurmayı, Google'ın neyi desteklediğini ve neyi kaldırdığını gördük ve hepsini testlere bağladık. Buradan sonraki doğal adım, işaretlediğin içeriğin kendisi: bu turda yazılan "Content SEO" serisi, Google'ın anlamaya çalıştığı içeriği ve sayfalar arası link yapısını ele alıyor.