Mert Özen Her Satırda Daha İleri
Frontend HTML5 ve Semantic HTML — #09

head ve Meta Tag'ler: charset, viewport, title, description, Open Graph, theme-color ve link rel'leri

Mert Özen 14 Kas 2026 14 dk 5 görüntülenme
head ve Meta Tag'ler: charset, viewport, title, description, Open Graph, theme-color ve link rel'leri

İyi bir head meta charset ile başlar, zoom'u kapatmayan tek bir viewport, sayfaya özel title ve tek bir description taşır, Open Graph tag'lerini de sunucudan gelen HTML'de verir. Bu yazıda charset'siz sayfanın Türkçe karakterleri bozduğunu, viewport'suz telefonun 980 piksel çizdiğini, JavaScript ile eklenen Open Graph'ı botun görmediğini, color-scheme'i ve Razor layout'ta tekrar eden meta'ları Nu Html Checker ve axe ile ölçüyoruz.

Sürüm notu: Bu yazı HTML (HyperText Markup Language) Living Standard'ın ve Baseline'ın Kasım 2026 durumuna göre yazıldı; örnekler Chrome 158, WebKit 27.2 (Playwright 1.65), axe-core 4.14, Nu Html Checker 26.11.9 ve .NET 10 (ASP.NET Core Razor Pages) ile denenmiştir.

Önce Tahmin Et

Bookstore'un ASP.NET Core layout'u yıllar içinde birkaç kişinin elinden geçmiş. Kitap detay sayfasının render edilmiş head'i şöyle:

{{site/layout-bug.html}}

Sayfa tarayıcıda düzgün görünüyor. Bu head'de kaç sorun var? Telefonda hangi viewport geçerli, kullanıcı ekranı iki parmağıyla büyütebilir mi? Google hangi description'ı görür? Ve Nu Html Checker bunlardan hangilerini yakalar?

Bir önceki yazı olan "Image ve Media: img, picture, srcset/sizes, video, audio, track ve iframe" ile sayfanın gövdesini bitirdik. Bu yazı görünmeyen kısım hakkında: head. Kullanıcı onu hiç görmez, ama Türkçe karakterlerin doğru çıkması, sayfanın telefonda okunabilir olması, bir link paylaşıldığında görünen kart ve tarayıcının adres çubuğunun rengi buradaki birkaç satıra bağlı. title ve description'ın arama sonuçlarındaki rolü SEO (Search Engine Optimization) Temelleri serisinin "Sonuç Sayfasında Görünmek: Title Link, Snippet, Meta Description ve Site Name" yazısında; canonical ve hreflang Teknik SEO serisinde. Burada HTML tarafını kuruyoruz.

Kitap Detay Sayfasının Eksiksiz head'i

Önce hedef. Bookstore'un kitap sayfası için kullandığım head:

{{site/book.html}}

Sıra rastgele değil. charset en başta, çünkü tarayıcı geri kalanı onunla okuyacak. viewport hemen arkasında, çünkü layout'u etkiliyor. title ve description sayfanın kimliği. Sonra renk ve ikon bilgileri, sonra kaynaklar: bağlantıyı önceden açan preconnect, stylesheet ve defer'li script (script yükleme stratejisi JavaScript Temel serisinin "JavaScript ile İlk Script: script Etiketi, defer, async ve DevTools" yazısında). En sonda paylaşım için Open Graph. Tarayıcı açısından charset dışındakilerin sırası kesin bir kural değil; ama bu düzen, bir code review'da eksik olanı görmeyi kolaylaştırıyor.

meta charset ve İlk 1024 Byte

Tarayıcı byte'ları karaktere çevirmek için encoding'i bilmek zorunda. Sırasıyla şuralara bakar: dosyanın başındaki BOM (Byte Order Mark), HTTP (HyperText Transfer Protocol) Content-Type header'ındaki charset, ve dokümanın ilk 1024 byte'ında bulduğu meta charset. Spec, meta charset'in bu 1024 byte'ın içinde tamamen bitmesini şart koşar. Dört sayfayı, Content-Type'ı charset'siz gönderen bir sunucuyla iki motorda denedim:

const pw = require('playwright');
const http = require('http');
const fs = require('fs');

const page = (beforeTitle, meta = '<meta charset="utf-8">') => `<!doctype html>
<html lang="tr">
<head>
    ${beforeTitle}
    ${meta}
    <title>Kürk Mantolu Madonna | Bookstore</title>
</head>
<body>
    <h1>Kürk Mantolu Madonna</h1>
</body>
</html>`;

const pages = {
  '/meta-first': { html: page(''), type: 'text/html' },
  // meta charset'ten önce 1100 byte'lık bir yorum (ör. büyük bir inline CSS bloğu)
  '/meta-late': { html: page(`<!-- ${'x'.repeat(1100)} -->`), type: 'text/html' },
  '/no-meta': { html: page('', ''), type: 'text/html' },
  // meta yok, ama sunucu charset'i header'da söylüyor
  '/no-meta-header': { html: page('', ''), type: 'text/html; charset=utf-8' },
};

// aynı sayfaları Nu Html Checker için diske de yaz
fs.mkdirSync('site/charset', { recursive: true });
for (const [url, p] of Object.entries(pages)) fs.writeFileSync(`site/charset${url}.html`, p.html);

const server = http.createServer((req, res) => {
  const p = pages[req.url];
  res.setHeader('Content-Type', p.type);
  res.end(Buffer.from(p.html, 'utf8'));
}).listen(18890);

(async () => {
  for (const name of ['chromium', 'webkit']) {
    const browser = await pw[name].launch();
    const tab = await browser.newPage();
    console.log(`--- ${name} ${browser.version()}`);
    for (const url of Object.keys(pages)) {
      await tab.goto(`http://localhost:18890${url}`);
      const [charset, h1] = await tab.evaluate(() => [document.characterSet, document.querySelector('h1').textContent]);
      console.log(`${url.padEnd(16)} ${charset.padEnd(13)} ${h1}`);
    }
    await browser.close();
  }
  server.close();
})();
--- chromium 158.0.8127.18
/meta-first      UTF-8         Kürk Mantolu Madonna
/meta-late       UTF-8         Kürk Mantolu Madonna
/no-meta         windows-1252  Kürk Mantolu Madonna
/no-meta-header  UTF-8         Kürk Mantolu Madonna
--- webkit 27.2
/meta-first      UTF-8         Kürk Mantolu Madonna
/meta-late       UTF-8         Kürk Mantolu Madonna
/no-meta         windows-1254  Kürk Mantolu Madonna
/no-meta-header  UTF-8         Kürk Mantolu Madonna

/no-meta satırı asıl tehlikeyi gösteriyor: ne header'da ne meta'da encoding var ve iki motor da kendi varsayılan legacy encoding'ine düştü. "Kürk" "Kürk" oldu; Türkçe bir sitede başlıkların, isimlerin ve adreslerin bozulması demek. Varsayılan encoding'in motordan motora (burada windows-1252 ve windows-1254) değişmesi de hatayı test ortamında görüp üretimde görmemeye, ya da tersine, yol açabilir. /no-meta-header satırında aynı sayfa, sunucu charset=utf-8 dediği için düzgün.

/meta-late ilginç: meta charset 1100 byte'lık bir yorumun arkasında kaldı, ama iki motor da doğru encoding'i, yani UTF (Unicode Transformation Format) ailesinden UTF-8'i buldu. Bugünkü tarayıcılar geç gelen bildirimi fark edip encoding'i parse sırasında değiştirebiliyor. Bu kurtarma bir garanti değil, bir tolerans; serinin ilk yazısında gördüğümüz hata affeden parser'ın bir başka örneği. Checker bunu error olarak işaretliyor (çıktı aşağıda) ve doğru olan da bu: meta charset head'in ilk satırı olsun, öncesine büyük bir inline style, yorum ya da title koyma.

ASP.NET Core kullanıyorsan iyi haber: Razor view'ları ve Razor Pages yanıtı Content-Type: text/html; charset=utf-8 ile gönderiyor, "Üretimde Dikkat"teki curl çıktısında göreceksin. Yine de meta charset'i yaz; sayfa diske kaydedildiğinde, bir CDN (Content Delivery Network) header'ı değiştirdiğinde ya da HTML statik dosya olarak servis edildiğinde header'a güvenemezsin.

viewport: width=device-width ve Zoom'u Kapatmamak

Mobil tarayıcılar, viewport bildirmeyen bir sayfayı masaüstü için yazılmış sayar ve onu genellikle 980 piksel genişliğinde çizip ekrana sığacak kadar küçültür. Sonuç, okunamayacak kadar küçük yazı. Üç varyantı 390 piksellik bir telefon emülasyonunda ölçtüm ve her birine axe-core'un meta-viewport kuralını çalıştırdım:

const { chromium } = require('playwright');
const fs = require('fs');

const book = fs.readFileSync('site/book.html', 'utf8');
const variants = {
  'no viewport': book.replace(/ *<meta name="viewport"[^>]*>\n/, ''),
  'device-width': book,
  'no zoom': book.replace('initial-scale=1', 'initial-scale=1, maximum-scale=1, user-scalable=no'),
};

(async () => {
  const browser = await chromium.launch();
  // 390 piksellik bir telefonu taklit et (isMobile viewport meta'yı devreye sokar)
  const context = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
  for (const [name, html] of Object.entries(variants)) {
    const page = await context.newPage();
    await page.setContent(html, { waitUntil: 'domcontentloaded' });
    await page.addScriptTag({ path: require.resolve('axe-core') });
    const [width, violations] = await page.evaluate(async () => {
      const r = await axe.run(document, { runOnly: ['meta-viewport'] });
      return [window.innerWidth, r.violations.map(v => `${v.id} (${v.impact})`)];
    });
    console.log(`${name.padEnd(13)} innerWidth=${width} axe: ${violations.join(', ') || 'no violations'}`);
  }
  await browser.close();
})();
no viewport   innerWidth=980 axe: no violations
device-width  innerWidth=390 axe: no violations
no zoom       innerWidth=390 axe: meta-viewport (moderate)

viewport olmadan layout genişliği 980 piksel; width=device-width ile 390, yani cihazın kendi genişliği. Responsive CSS'teki (Cascading Style Sheets) media query'ler ancak bu satır varken telefonda doğru çalışır. initial-scale=1 açılıştaki zoom seviyesini 1 yapar.

Üçüncü satır, "mobilde sayfa yanlışlıkla zoom'lanmasın" diye eklenen maximum-scale=1, user-scalable=no. Layout aynı, ama axe bir ihlal buldu: bu ayar kullanıcının iki parmakla büyütmesini engelliyor. Az gören bir kullanıcı için bu, sayfayı okuyamamak demek. WCAG (Web Content Accessibility Guidelines) metnin en az iki kat büyütülebilmesini ister. MDN'e (MDN Web Docs) göre iOS 10'dan beri Safari bu ayarı varsayılan olarak yok sayıyor, yani bu satır iPhone'da zaten çalışmıyor; ama Android'de çalışabiliyor. Doğru satır kısa: width=device-width, initial-scale=1, ve zoom'u sınırlayan hiçbir şey.

title ve meta description: HTML Tarafı

title HTML'in zorunlu element'lerinden biri: tarayıcı sekmesinde, geçmişte, yer imlerinde ve ekran okuyucunun sayfaya girerken okuduğu ilk cümlede o var. İyi bir title en ayırt edici bilgiyle başlar ve site adıyla biter: "Clean Architecture by Robert C. Martin | Bookstore". Sırayı ters çevirirsen, on sekme açık olan bir kullanıcı hepsinde "Bookstore | ..." görür.

meta name="description" sayfanın kısa özetidir ve arama sonucundaki snippet için bir aday. Uzunluğu, Google'ın onu ne zaman kullanıp ne zaman kendi snippet'ini ürettiği SEO Temelleri serisinin konusu. HTML tarafında iki kural yeter: her sayfanın kendi description'ı olsun ve bir sayfada yalnız bir tane olsun. Tahmin sorusundaki sayfada iki tane var; hangisinin kullanılacağı arama motoruna kalmış ve checker bunu error olarak işaretliyor.

Open Graph ve Twitter Card'ları

Bir kitap linki WhatsApp, LinkedIn, Slack ya da X'te paylaşıldığında görünen kartı Open Graph tag'leri belirler. Protokol dört property'yi zorunlu sayar: og:title, og:type, og:image ve og:url. og:image varsa, ogp.me'ye göre görseli anlatan bir og:image:alt da olmalı. Bizim sayfada og:type book; protokolün kitaplar için tanımladığı bu tür book:isbn gibi ek property'ler alıyor.

Görsel için Meta'nın (Facebook) önerisi en az 1200×630 piksel, yaklaşık 1.91:1 oranı; 600×315'in altında kart küçük görsele düşüyor. og:image:width ve og:image:height'ı yazmak da iş görür: Meta'nın dokümanına göre crawler bu sayede görseli indirip işlemeyi beklemeden kartı çizebiliyor, yoksa linki ilk paylaşan kişi görselsiz bir kart görebiliyor. Crawler görselleri URL'e (Uniform Resource Locator) göre cache'liyor; görseli değiştirdiğinde yeni bir URL ver. X tarafında twitter:card ile kart türünü (summary_large_image) seçmen yeterli; başlık, açıklama ve görsel için X, twitter:* karşılığı yoksa Open Graph değerlerine düşüyor.

Bu tag'lerle ilgili en pahalı hata, onları JavaScript ile eklemek. Angular gibi client-side render eden bir uygulamada başlığı ve meta'ları route değiştikçe bir service ile güncellemek kolaydır, ama link önizlemesi yapan bir bot sayfayı tarayıcı gibi çalıştırmaz:

{{site/csr.html}}
const { chromium } = require('playwright');
const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  if (!req.url.endsWith('.html')) { res.statusCode = 404; return res.end(); }
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
  res.end(fs.readFileSync(`site${req.url}`));
}).listen(18893);

// link önizlemesi yapan bir bot gibi: HTML'i indir, JavaScript çalıştırmadan meta'ları oku
const ogTags = html => [...html.matchAll(/<meta property="(og:[^"]+)" content="([^"]*)"/g)].map(m => m[1]);
const title = html => html.match(/<title>([^<]*)<\/title>/)[1];

(async () => {
  const browser = await chromium.launch();
  for (const file of ['/book.html', '/csr.html']) {
    const raw = await (await fetch(`http://localhost:18893${file}`)).text();
    console.log(`${file} raw HTML : title="${title(raw)}" og=[${ogTags(raw).join(', ')}]`);
    const page = await browser.newPage();
    await page.goto(`http://localhost:18893${file}`);
    const dom = await page.content();
    console.log(`${file} browser  : title="${title(dom)}" og=[${ogTags(dom).join(', ')}]`);
    await page.close();
  }
  await browser.close();
  server.close();
})();
/book.html raw HTML : title="Clean Architecture by Robert C. Martin | Bookstore" og=[og:type, og:title, og:description, og:url, og:image, og:image:width, og:image:height, og:image:alt, og:site_name]
/book.html browser  : title="Clean Architecture by Robert C. Martin | Bookstore" og=[og:type, og:title, og:description, og:url, og:image, og:image:width, og:image:height, og:image:alt, og:site_name]
/csr.html raw HTML : title="Bookstore" og=[]
/csr.html browser  : title="Clean Architecture by Robert C. Martin | Bookstore" og=[og:title, og:image]

Tarayıcıda iki sayfa da doğru görünüyor: csr.html'in başlığı ve Open Graph tag'leri JavaScript çalıştıktan sonra yerinde. Ama yalnız HTML'i indiren bir bot için csr.html'in başlığı "Bookstore" ve tek bir Open Graph tag'i yok; paylaşılan link boş bir kartla görünür. Link önizlemesi yapan botların çoğu böyle çalışır. Çözüm, bu tag'lerin sunucudan gelen HTML'de olması: ASP.NET Core'da Razor zaten öyle üretiyor, Angular'da ise SSR (Server-Side Rendering) ya da prerender gerekir. Aynı ilke AI (artificial intelligence) agent'lar ve crawler'lar için de geçerli; GEO (Generative Engine Optimization) serisinin "AI Agent'lar İçin Site" yazısında. Sayfanın JSON (JavaScript Object Notation) tabanlı JSON-LD formatıyla verilen yapılandırılmış verisi ise ilerideki Structured Data ve Schema.org serisinin konusu.

color-scheme ve theme-color

meta name="color-scheme" content="light dark" tarayıcıya "bu sayfa iki temayı da destekliyor" der. Etkisi senin CSS'inden önce, tarayıcının kendi stillerinde görünür:

const { chromium } = require('playwright');
const fs = require('fs');

const book = fs.readFileSync('site/book.html', 'utf8');
const variants = {
  'with color-scheme': book,
  'without': book.replace(/ *<meta name="color-scheme"[^>]*>\n/, ''),
};

(async () => {
  const browser = await chromium.launch();
  for (const [name, html] of Object.entries(variants)) {
    const page = await browser.newPage();
    // kullanıcının işletim sistemi dark mode'da
    await page.emulateMedia({ colorScheme: 'dark' });
    await page.setContent(html, { waitUntil: 'domcontentloaded' });
    const r = await page.evaluate(() => {
      const css = el => getComputedStyle(el);
      return `text=${css(document.body).color} input=${css(document.querySelector('input')).backgroundColor}`;
    });
    console.log(`${name.padEnd(18)} ${r}`);
  }
  await browser.close();
})();
with color-scheme  text=rgb(255, 255, 255) input=rgb(59, 59, 59)
without            text=rgb(0, 0, 0) input=rgb(255, 255, 255)

Kullanıcının işletim sistemi dark mode'dayken, color-scheme bildiren sayfada tarayıcı varsayılan metni beyaza, input'un zeminini koyu griye çekti. Bildirmeyen sayfada her şey açık temada kaldı: dark bir tasarımın ortasında bembeyaz input'lar ve scrollbar'lar görmenin sebebi genellikle bu satırın eksikliğidir. Satır HTML'de olduğu için, CSS dosyası daha inmeden ilk boyamada da etkili. Dark mode'un CSS tarafı ilerideki CSS3: Flexbox, Grid ve Modern CSS serisinde.

theme-color tarayıcının arayüzüne (adres çubuğu, sekme şeridi) bir renk önerir ve media attribute'u ile açık ve koyu tema için ayrı değer alabilir. Ama desteği dağınık: caniuse verisine göre Chrome'da kısmi (ağırlıkla Android ve kurulu uygulamalar), Firefox'ta yok, ve Safari iOS 26 ile iPhone'da kullanmayı bıraktı. Yani theme-color bir süs; sayfanın hiçbir şeyi ona bağlı olmamalı. PWA (Progressive Web App) olarak kurulan uygulamada manifest'teki theme_color da devreye girer; meta tag onun üstüne yazar. Manifest'in kendisi ilerideki Browser API'leri (Application Programming Interface) ve PWA serisinde.

İkonlar için bugün üç satır yeterli: eski tarayıcılar ve araçlar için 32×32'lik bir favicon.ico, modern tarayıcılar için ölçeklenen (ve istersen içinde prefers-color-scheme media query'si barındırabilen) bir SVG (Scalable Vector Graphics), iOS ana ekranı için 180×180 piksellik bir apple-touch-icon PNG'si (Portable Network Graphics). Android ve kurulabilir uygulama ikonları manifest'ten gelir. head'deki link element'lerinin geri kalanı bu serinin değil, başka serilerin konusu; kısa bir harita:

relNe işe yararAyrıntı
icon, apple-touch-iconSekme, yer imi ve ana ekran ikonuBu yazı
stylesheetCSS dosyası; render'ı bloklarCSS3 serisi, Web Performansı
preconnect, preload, modulepreloadBağlantıyı ya da kaynağı önceden hazırlarWeb Performansı
canonicalSayfanın asıl URL'ini bildirirTeknik SEO
alternate + hreflangSayfanın diğer dillerdeki karşılığıTeknik SEO
manifestKurulabilir uygulama bilgileriBrowser API'leri ve PWA

Üretimde Dikkat

Tahmin sorusundaki sayfa, tüm sayfaların aynı layout'u paylaştığı bir uygulamada nasıl ortaya çıkar? Layout'a bir "varsayılan" description ve viewport konur, sonra bir sayfa kendi değerini head'e ikinci kez ekler; ya da hiçbir sayfa kendi değerini vermez ve bütün site aynı description'la yayınlanır. Razor Pages'te ikisini de önleyen layout:

{{razor/Pages/Shared/_Layout.cshtml}}
{{razor/Pages/Books/Detail.cshtml}}
{{razor/Pages/Cart.cshtml}}

charset ve viewport yalnız layout'ta, bir kez. description sayfa veriyorsa basılıyor, vermiyorsa hiç basılmıyor: aynı cümlenin yüzlerce sayfada tekrar etmesindense eksik olması daha iyi, çünkü eksik olanı bir testle bulabilirsin. Open Graph gibi sayfaya özel tag'ler bir section ile geliyor. curl ile iki sayfa:

curl -si http://localhost:5291/books/clean-architecture
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Date: Fri, 13 Nov 2026 22:02:47 GMT
Server: Kestrel
Transfer-Encoding: chunked

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Clean Architecture by Robert C. Martin | Bookstore</title>
        <meta name="description" content="Clean Architecture explains how to keep business rules independent of frameworks &amp; databases.">
    
    <meta property="og:title" content="Clean Architecture">
    <meta property="og:image" content="https://images.bookstore.example/og/clean-architecture.jpg">

</head>
<body>
    <main>
    <h1>Clean Architecture</h1>
</main>

</body>
</html>
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Your cart | Bookstore</title>
    
</head>

İki ayrıntı: Content-Type header'ında charset=utf-8 var, ve Razor description'daki &'yi attribute içinde &amp; olarak encode etti. Değeri string birleştirerek elle basan bir kod bunu yapmaz ve bir tırnak işareti attribute'u bozar. Sepet sayfasında description yok; arama motorunda indekslenmesini istemediğin böyle bir sayfa için bu bilinçli bir tercih olabilir, ama kitap sayfası gibi önemli sayfalar için bir integration test ya da build sonrası bir kontrol eksik description'ı yakalamalı.

Son olarak checker. Yazıdaki sayfaları Nu Html Checker'dan geçirdim:

docker run --rm -v "${PWD}/site:/site" ghcr.io/validator/validator:latest vnu --skip-non-html /site
"file:/site/layout-bug.html":10.5-10.121: error: A document must not include more than one “meta” element with its “name” attribute set to the value “description”.
"file:/site/charset/meta-late.html":5.5-5.25: error: A “charset” attribute on a “meta” element found after the first 1024 bytes.

Checker iki şeyi yakaladı: ikinci description ve 1024 byte'tan sonra gelen charset. Ama tahmin sorusundaki ikinci viewport'u, zoom'u kapatan maximum-scale=1'i ve title'ın charset'ten önce gelmesini işaretlemedi (o sayfada charset hâlâ ilk 1024 byte'ın içinde). Aynı sayfaya axe'ı çalıştırınca eksik yarı ortaya çıkıyor:

const { chromium } = require('playwright');
const fs = require('fs');

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
  const page = await context.newPage();
  await page.setContent(fs.readFileSync('site/layout-bug.html', 'utf8'));
  await page.addScriptTag({ path: require.resolve('axe-core') });
  console.log(await page.evaluate(async () => {
    const r = await axe.run(document, { runOnly: ['meta-viewport'] });
    return r.violations.map(v => `${v.id}: ${v.nodes.map(n => n.html).join(' | ')}`).join('\n') || 'no violations';
  }));
  await browser.close();
})();
meta-viewport: <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

İki araç farklı soruları cevaplıyor: checker spec'e uygunluğu, axe erişilebilirliği. head için ikisini birlikte çalıştır.

Sık Düşülen Tuzaklar

Birincisi, meta keywords yazmak. Google 2009'da bu tag'i web aramasındaki sıralamada kullanmadığını açıkladı; bugün tek etkisi rakiplere anahtar kelime listeni vermek.

İkincisi, birden fazla viewport. Tahmin sorusundaki ikincisi zoom'u kapatıyordu ve checker bunu görmedi. Layout'ta bir tane olsun, sayfalar ona dokunmasın.

Üçüncüsü, layout'ta her sayfaya aynı description'ı basmak. Arama motoru için yüzlerce aynı özet, hiç özet olmamasından iyi değildir.

Dördüncüsü, user-scalable=no ya da maximum-scale=1. Erişilebilirlik ihlali ve iPhone'da zaten yok sayılıyor.

Beşincisi, Open Graph tag'lerini yalnız client-side'da eklemek. Botlar JavaScript çalıştırmadan okuduğu için kart boş kalır.

Altıncısı, meta charset'ten önce büyük bir inline style ya da title koymak. Bugünkü tarayıcılar çoğu zaman kurtarıyor; kurtarmalarına güvenme.

Küçük Bir Deneme

★☆☆ Çalıştığın bir sitenin ana sayfasının ve bir detay sayfasının kaynağını (DevTools'ta değil, View Source'ta; yani sunucudan gelen HTML'de) aç. charset ilk satırda mı, kaç tane viewport ve description var, zoom'u kapatan bir değer var mı?

★★☆ crawler.js'i kendi sitenin üç sayfasına uyarla: ham HTML'deki title ve Open Graph tag'lerini tarayıcıdaki DOM (Document Object Model) ile karşılaştır. Fark varsa hangi sayfa türünde olduğunu ve sebebini bul.

★★★ Razor ya da Angular uygulamanda, her sayfa için title, tek bir description, og:title, og:image ve og:image:alt'ın sunucudan gelen HTML'de bulunduğunu doğrulayan bir integration test yaz. Test, layout'a yanlışlıkla eklenen ikinci bir viewport'u da yakalasın.

head ile sayfanın kimliği tamam. Bir sonraki yazıda gövdeye geri dönüp en çok yanlış kullanılan element'lerden birine bakıyoruz: table. Bookstore'un sipariş geçmişi tablosunu caption, thead, th scope ve colgroup ile kurup dar ekranda tablo semantiğini bozmadan nasıl göstereceğimizi göreceğiz.