---
title: "WordPress'ten Astro'ya: 3 MB'lık sayfa 300 KB'a, Lighthouse'ta 98 puan"
author: "Abdulaziz Akyol"
author_url: https://www.abdulazizakyol.com/hakkimda/
url: https://www.abdulazizakyol.com/blog/astro-ve-cloudflare-ile-hizli-kisisel-site-core-web-vitals/
language: tr
published: 2026-09-25
categories: ["Yazılım geliştirme"]
tags: ["Astro", "Core Web Vitals", "LCP", "WebP", "Cloudflare Pages", "Lighthouse"]
translation: https://www.abdulazizakyol.com/en/blog/a-fast-personal-site-with-astro-and-cloudflare-core-web-vitals/
description: "Bu siteyi WordPress'ten statik Astro'ya taşıdık: mobil LCP 16,8 sn'den 1,9 sn'ye, sayfa 2.975 KiB'den 309 KiB'a indi. Teknikler ve gerçek kodlar."
---

# WordPress'ten Astro'ya: 3 MB'lık sayfa 300 KB'a, Lighthouse'ta 98 puan

Yazar: [Abdulaziz Akyol](https://www.abdulazizakyol.com/hakkimda/) · 2026-09-25

## Öne çıkanlar

- abdulazizakyol.com, eski bir WordPress kurulumundan Astro 5 ile üretilen tamamen statik bir siteye taşındı ve Cloudflare Pages'te yayınlanıyor; sunucuda PHP, veritabanı ve yönetim paneli yok.
- Mobil Lighthouse ölçümünde ana sayfanın performans puanı 62'den 98'e çıktı, LCP 16,8 saniyeden 1,9 saniyeye, sayfa ağırlığı 2.975 KiB'den 309 KiB'a indi.
- Kazancın büyük kısmı görsellerden geldi: her JPEG/PNG için derleme sonunda 360–1920 px arası WebP varyantları üretildi ve srcset/sizes ile tarayıcının ekrana uygun dosyayı seçmesi sağlandı.
- LCP görseli CSS background-image yerine fetchpriority="high" olan bir <img> ve imagesrcset'li bir preload ile verildiğinde tarayıcı onu HTML'i okurken hemen keşfeder.
- Fontların siteyle birlikte sunulması, CSS'in sayfaya gömülmesi ve YouTube oynatıcısının tıklanınca yüklenmesi render'ı engelleyen harici istekleri kaldırdı; CLS tüm sayfalarda 0 ölçüldü.

Bu yazı, abdulazizakyol.com'un eski bir WordPress kurulumundan Astro 5 ile üretilen tamamen statik bir siteye taşınmasının ve ardından yapılan Core Web Vitals çalışmasının vaka çalışmasıdır. Mobil Lighthouse ölçümünde ana sayfanın performans puanı 62'den 98'e çıktı; en büyük içerikli boyama (Largest Contentful Paint, LCP) 16,8 saniyeden 1,9 saniyeye, sayfa ağırlığı 2.975 KiB'den 309 KiB'a indi.

Aşağıda her tekniği "neden, nasıl, hangi kod" sırasıyla anlatıyorum. Kod parçaları sitenin gerçek deposundan alındı; kısalttığım yerleri belirttim.

## Sorun neydi?

Kişisel bir sitede WordPress'in asıl maliyeti barındırma ücreti değil, bakım yüküdür: çekirdek, tema ve eklenti güncellemeleri, her eklentinin getirdiği ayrı güvenlik riski ve bir eklenti kaldırıldığında bozulan sayfalar. İçeriği sık değişmeyen bir site için bu yük orantısız.

İkinci sorun hızdı. Mobilde ana sayfa yaklaşık 3 MB veri indiriyordu ve en büyük görsel ekrana 16,8 saniyede geliyordu. Mobil ağda bu, ziyaretçinin uzun süre boş ya da yarım bir sayfaya bakması demek.

Karar: sunucuda hiçbir şey çalıştırmayan statik bir site. Astro 5 her sayfayı derleme sırasında HTML'e çeviriyor, çıktı Cloudflare Pages'e yükleniyor. PHP, veritabanı ve yönetim paneli yok; saldırılacak bir giriş formu ya da güncellenmesi unutulacak bir eklenti de yok. Yazılar depoda Markdown dosyası olarak duruyor.

## Nasıl ölçtük?

Tüm ölçümler canlı sitede, Lighthouse'un mobil profiliyle (simüle 4G) yapıldı. "Önce" değerleri eski WordPress sitesinden, "sonra" değerleri yeni siteden. İki uyarı önemli:

1. **Lighthouse puanı bir aralıktır.** Ana sayfa ölçümler arasında 87 ile 98 arasında dalgalandı. Chrome ekibi de puanı tek bir sayı yerine dağılım olarak düşünmeyi öneriyor ([Lighthouse performance scoring](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring)).
2. **Lighthouse bir laboratuvar ölçümüdür.** Core Web Vitals'ın asıl değerlendirmesi gerçek kullanıcı verisinin 75. yüzdelik dilimi üzerinden yapılır: LCP ≤ 2,5 sn, Interaction to Next Paint (INP) ≤ 200 ms, Cumulative Layout Shift (CLS) ≤ 0,1 ([web.dev: Web Vitals](https://web.dev/articles/vitals)). Lighthouse sayfa yüklemesinde INP ölçmez; ona en yakın laboratuvar göstergesi Total Blocking Time'dır (TBT).

Lighthouse 10 ile performans puanındaki ağırlıklar şöyle: TBT %30, LCP %25, CLS %25, First Contentful Paint ve Speed Index %10'ar. LCP ve CLS birlikte puanın yarısını oluşturuyor; bu yüzden görsellerle uğraşmak en çok getiren iş oldu.

## Yapılanlar

### 1. Görseller: derleme sonunda WebP varyantları

**Neden:** Sayfa ağırlığının büyük kısmı fotoğraflardı. Telefona 1920 px genişliğinde bir JPEG göndermek, ekranın kullanabileceğinin birkaç katı veri demek. Astro'nun `Image` ve `Picture` bileşenleri yalnızca `src/` altındaki görselleri işler; `public/` içindekiler olduğu gibi kopyalanır. Eski WordPress adreslerini (`/uploads/2020/07/...`) bozmamak için görseller `public/uploads` altında kaldı, bu yüzden kendi adımımızı yazdık.

**Nasıl:** Derleme bittiğinde çalışan küçük bir Astro entegrasyonu (`astro:build:done` kancası), [sharp](https://sharp.pixelplumbing.com/) ile her JPEG/PNG'den 360, 540, 720, 960, 1280, 1600 ve 1920 px genişliğinde WebP varyantları üretiyor; kaynaktan büyük varyant üretilmiyor. Çıktılar `node_modules/.cache` altında saklanıyor: 83 görsel ilk derlemede yaklaşık 5 saniyede işleniyor, sonraki derlemelerde önbellekten anında kopyalanıyor.

Kurallar tek dosyada; entegrasyon ve bileşenler aynı listeyi kullanıyor:

```javascript
// src/lib/responsive.mjs
// /uploads/2020/07/foto.jpeg  →  /_img/2020/07/foto-640.webp, -960.webp, ...
export const WIDTHS = [360, 540, 720, 960, 1280, 1600, 1920];
export const QUALITY = 72;

export const isOptimizable = (src) => typeof src === 'string' && /^\/uploads\/.+\.(jpe?g|png)$/i.test(src);

/** Orijinal genişliğe göre üretilecek varyant genişlikleri (büyütme yapılmaz). */
export function variantWidths(originalWidth) {
  const ws = WIDTHS.filter((w) => w < originalWidth);
  ws.push(Math.min(originalWidth, WIDTHS[WIDTHS.length - 1]));
  return [...new Set(ws)];
}

export const variantPath = (src, w) => src.replace(/^\/uploads\//, '/_img/').replace(/\.(jpe?g|png)$/i, `-${w}.webp`);

export const srcsetFor = (src, originalWidth) =>
  variantWidths(originalWidth).map((w) => `${encodeURI(variantPath(src, w))} ${w}w`).join(', ');
```

Varyant üretimi, altı paralel işle (kısaltılmış):

```javascript
// integrations/responsive-images.mjs — buildVariants() içinden
for (const w of variantWidths(width)) {
  const rel = variantPath(src, w).slice(1);
  const cached = join(CACHE, rel), out = join(root, rel);
  if (!(await newer(cached, orig))) {            // önbellekte yoksa ya da kaynak daha yeniyse üret
    await mkdir(dirname(cached), { recursive: true });
    await sharp(orig).resize({ width: w, withoutEnlargement: true })
      .webp({ quality: QUALITY, effort: 5 }).toFile(cached);
  }
  await mkdir(dirname(out), { recursive: true });
  await copyFile(cached, out);                    // dist/_img/... altına kopyala
}
```

Bileşen tarafında `responsive(src, sizes)` yardımcısı `srcset`, `sizes`, `width` ve `height` döndürüyor. Boyutların HTML'de olması, görsel inmeden yerinin ayrılması, yani CLS'nin 0'da kalması demek:

```typescript
// src/lib/img.ts — kullanım: <img {...responsive(src, '(max-width: 560px) 100vw, 400px')} />
export function responsive(src: string, sizes: string) {
  const d = dims(src); // JPEG/PNG başlığından genişlik ve yükseklik
  if (!d || !isOptimizable(src)) return { src, ...d };
  return { src, srcset: srcsetFor(src, d.width), sizes, width: d.width, height: d.height };
}
```

### 2. Yazı içindeki görsellere otomatik srcset

**Neden:** Blog yazıları Markdown ve eski HTML içeriyor; her `<img>` etiketini elle düzenlemek sürdürülebilir değil.

**Nasıl:** Aynı entegrasyon üretilen her HTML dosyasını tarıyor ve `srcset`'i olmayan `/uploads/` görsellerine `srcset`, `sizes`, boyut, `decoding="async"` ve `loading="lazy"` ekliyor. `fetchpriority` verilmiş, yani LCP adayı olan görsellere `lazy` eklenmiyor:

```javascript
// integrations/responsive-images.mjs
const PROSE_SIZES = '(max-width: 820px) calc(100vw - 32px), 760px';

function rewriteImg(tag, meta) {
  const src = attr(tag, 'src');
  if (!src || attr(tag, 'srcset') !== undefined) return tag;
  const key = decodeURI(src);
  const m = meta.get(key);
  if (!m) return tag;
  const add = [];
  add.push(`srcset="${srcsetFor(key, m.width)}"`);
  if (attr(tag, 'sizes') === undefined) {
    const w = Number(attr(tag, 'width'));
    add.push(`sizes="${w && w <= 480 ? `${w}px` : PROSE_SIZES}"`);
  }
  if (attr(tag, 'width') === undefined && attr(tag, 'height') === undefined) add.push(`width="${m.width}" height="${m.height}"`);
  if (attr(tag, 'decoding') === undefined) add.push('decoding="async"');
  if (attr(tag, 'loading') === undefined && attr(tag, 'fetchpriority') === undefined) add.push('loading="lazy"');
  return tag.replace(/^<img/, `<img ${add.join(' ')}`);
}
```

### 3. LCP görseli: background-image yerine img ve preload

**Neden:** Hero ve sayfa başlığı görselleri CSS `background-image` olarak veriliyordu. Tarayıcının ön yükleme tarayıcısı (preload scanner) HTML'i okurken CSS içindeki görseli göremez; görsel ancak stil hesaplandıktan sonra istenir. web.dev'in [LCP rehberi](https://web.dev/articles/optimize-lcp) bu bekleyişi "kaynak yükleme gecikmesi" (resource load delay) olarak ayrı bir kalemde ele alıyor ve LCP görselinin asla lazy-load edilmemesini öneriyor.

**Nasıl:** Görsel artık gerçek bir `<img>`: `fetchpriority="high"`, `srcset` ve `sizes` ile. `<head>` içinde de `imagesrcset`'li bir preload var; tarayıcı hangi genişliği indireceğine HTML'in başında karar veriyor:

```astro
---
// src/layouts/Base.astro (ilgili satırlar)
// Sayfa başlığı görseli: iç sayfalarda LCP öğesi. <img> + preload ile erken keşfedilir.
const bannerImg = !hero && banner ? responsive(banner, '100vw') : undefined;
---
<head>
  <link rel="preload" href="/fonts/manrope-latin.woff2" as="font" type="font/woff2" crossorigin />
  <link rel="preload" href="/fonts/big-shoulders-display-latin.woff2" as="font" type="font/woff2" crossorigin />
  {bannerImg?.srcset && <link rel="preload" as="image" imagesrcset={bannerImg.srcset} imagesizes="100vw" fetchpriority="high" />}
</head>
<body>
  <header class="page-head">
    {bannerImg && <img class="bg" {...bannerImg} alt="" fetchpriority="high" decoding="async" />}
  </header>
</body>
```

Ana sayfa hero'sunda görsel dikey ekranda genişliği değil yüksekliği doldurduğu için `sizes` değeri `(orientation: portrait) 75vh, 100vw`. Bu ayrıntı küçük görünür ama telefonda tarayıcının doğru varyantı seçmesini sağlar.

### 4. Diğer hero slaytları: sırası gelince

**Neden:** Slayt gösterisindeki tüm görseller ilk yüklemede inerse LCP görseliyle bant genişliği için yarışır.

**Nasıl:** İlk slayt HTML ile geliyor; diğerlerinin `srcset`'i `data-srcset` içinde bekliyor ve sayfa yüklendikten sonra, sırası gelmeden hemen önce atanıyor. Veri tasarrufu (Save-Data) açıksa, bağlantı 2G ise ya da kullanıcı hareketin azaltılmasını (`prefers-reduced-motion`) seçtiyse slaytlar hiç dönmüyor:

```typescript
// src/views/Home.astro — <script>
const hero = document.querySelector<HTMLElement>('.hero');
const slides = [...document.querySelectorAll<HTMLImageElement>('.slide')];
const conn = (navigator as any).connection;
const quiet = matchMedia('(prefers-reduced-motion: reduce)').matches || conn?.saveData || /(^|-)2g$/.test(conn?.effectiveType ?? '');
if (hero && slides.length > 1 && !quiet) {
  const every = Number(hero.dataset.interval) || 5000;
  const load = (img: HTMLImageElement) => {
    if (img.dataset.srcset) { img.srcset = img.dataset.srcset; delete img.dataset.srcset; }
    return img.decode().catch(() => {});
  };
  let i = 0;
  const tick = async () => {
    if (document.hidden) return;
    const n = (i + 1) % slides.length;
    await load(slides[n]);
    slides[i].classList.remove('active'); slides[n].classList.add('active'); i = n;
    load(slides[(n + 1) % slides.length]);
  };
  addEventListener('load', () => { setTimeout(() => load(slides[1]), every - 1500); setInterval(tick, every); }, { once: true });
}
```

### 5. Fontlar: siteyle birlikte woff2 ve iki preload

**Neden:** Google Fonts, ayrı bir alan adına bağlantı ve render'ı engelleyen bir CSS isteği demek.

**Nasıl:** Üç font ailesinin yalnızca latin ve latin-ext alt kümeleri woff2 olarak `/fonts` altında. Türkçedeki ğ, ş ve İ gibi harfler latin-ext kümesinde; `unicode-range` sayesinde bu dosya yalnızca gerektiğinde iniyor. İlk ekranda kullanılan iki dosya yukarıdaki `Base.astro` parçasında preload ediliyor. Font preload'unda `crossorigin` özniteliği şart; yoksa tarayıcı dosyayı iki kez indirir.

```css
/* src/styles/fonts.css — altı tanımdan biri */
@font-face {
  font-family: 'Manrope'; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('/fonts/manrope-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
```

### 6. CSS'i sayfaya gömmek

**Neden:** Harici CSS dosyası render'ı engeller; tarayıcı onu indirmeden sayfayı çizmez.

**Nasıl:** Astro'da tek ayar: `build.inlineStylesheets: 'always'`. Varsayılan `'auto'` yalnızca küçük stil dosyalarını gömer. Bu sitenin CSS'i küçük olduğu için her sayfaya gömmek ayrı bir istek beklemekten ucuz. CSS'i büyük sitelerde takas tersine dönebilir: aynı CSS her sayfada yeniden iner ve tarayıcı önbelleğinin avantajı kaybolur.

```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import responsiveImages from './integrations/responsive-images.mjs';
export default defineConfig({
  site: 'https://www.abdulazizakyol.com',
  // Tüm adresler sonunda "/" ile biter; Cloudflare Pages de bu biçimi sunar.
  // Böylece iç bağlantılar 308 yönlendirmesine takılmaz ve canonical tek olur.
  trailingSlash: 'always',
  // CSS küçük (~13 KB); sayfaya gömülmesi render'ı engelleyen istekleri kaldırır.
  build: { format: 'directory', inlineStylesheets: 'always' },
  integrations: [responsiveImages()],
});
```

### 7. YouTube: tıklanınca yüklenen önizleme

**Neden:** Gömülü YouTube iframe'i, kullanıcı oynat düğmesine hiç basmasa bile oynatıcının betiklerini ve stillerini indirir.

**Nasıl:** Sayfada yalnızca önizleme görseli ve oynat simgesi olan bir bağlantı var. Tıklanınca iframe `youtube-nocookie.com` adresiyle oluşturuluyor; JavaScript çalışmazsa bağlantı videoya YouTube'da açılıyor. Video ve iframe'in temelleri için [HTML5 medya etiketleri](https://www.abdulazizakyol.com/blog/html5-medya-etiketleri/) yazısına bakabilirsiniz.

```typescript
// src/views/StageItem.astro — <script>
document.querySelectorAll<HTMLAnchorElement>('a.yt').forEach((a) => a.addEventListener('click', (e) => {
  e.preventDefault();
  const f = document.createElement('iframe');
  f.src = `https://www.youtube-nocookie.com/embed/${a.dataset.id}?autoplay=1&rel=0`;
  f.title = a.dataset.title || '';
  f.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
  f.allowFullscreen = true;
  f.style.cssText = 'width:100%;height:100%;border:0';
  const box = document.createElement('div'); box.className = 'video';
  box.append(f);
  a.replaceWith(box);
}));
```

### 8. Cloudflare e-posta gizleme betiğini kapatmak

**Neden:** Cloudflare'in e-posta adresi gizleme (Email Address Obfuscation) özelliği, adresleri çözmek için sayfaya ayrı bir betik (`email-decode.min.js`) ekler. Adresi zaten herkese açık olan bir kişisel sitede bu fazladan bir istektir; üstelik adresi yapay zekâ tarayıcılarından da saklar.

**Nasıl:** Cloudflare'in [belgelediği](https://developers.cloudflare.com/waf/tools/scrape-shield/email-address-obfuscation/) `<!--email_off-->` ve `<!--/email_off-->` yorumları `Base.astro` içinde tüm gövdeyi sarıyor. Panel ayarına dokunmadan, kodla ve sürüm kontrolünde.

### 9. Erişilebilirlik ve otomatik mobil taşma testi

**Neden:** Lighthouse'un erişilebilirlik denetimi düşük kontrastı ve okunması zor küçük yazıları işaretler. Mobilde yatay kaydırma ise kullanıcı deneyimini doğrudan bozar ve gözle aramakla bulunamayacak kadar çok sayfa–genişlik bileşimi vardır.

**Nasıl:** Kontrastı yetersiz metinler ve 12 px altındaki yazılar düzeltildi. Taşma için puppeteer ile otomatik bir test yazıldı: 19 sayfa, 320–768 px arasında 5 genişlikte açılıyor ve görünüm alanının dışına taşan öğeler listeleniyor. İlk çalıştırmada 51 sorun çıktı; uzun URL ve kod satırlarının kırılması, başlıkların satır kırabilmesi gibi düzeltmelerle sayı 0'a indi. Anlamsal HTML'in erişilebilirliğe katkısı için [HTML5 anlamsal web](https://www.abdulazizakyol.com/blog/html-5-anlamsal-web/) yazısı iyi bir başlangıç. Testin basitleştirilmiş hâli:

```javascript
// mobil-tasma-testi.mjs — önce: npm i -D puppeteer; site yerelde çalışırken: node mobil-tasma-testi.mjs
import puppeteer from 'puppeteer';

const BASE = 'http://localhost:4321';
const PAGES = ['/', '/hakkimda/', '/blog/', '/en/'];   // kendi sayfa listeniz
const WIDTHS = [320, 360, 390, 414, 768];            // örnek genişlikler

const browser = await puppeteer.launch();
const page = await browser.newPage();
let problems = 0;
for (const path of PAGES) {
  for (const width of WIDTHS) {
    await page.setViewport({ width, height: 800 });
    await page.goto(BASE + path, { waitUntil: 'load' });
    const wide = await page.evaluate(() => {
      const vw = document.documentElement.clientWidth;
      if (document.documentElement.scrollWidth <= vw) return [];   // yatay kaydırma yok
      return [...document.querySelectorAll('body *')]
        .filter((el) => el.getBoundingClientRect().right > vw + 1)
        .slice(0, 5)
        .map((el) => el.tagName.toLowerCase() + (typeof el.className === 'string' && el.className.trim() ? '.' + el.className.trim().split(/\s+/).join('.') : ''));
    });
    if (wide.length) { problems += wide.length; console.log(`${path} @ ${width}px →`, wide.join(', ')); }
  }
}
await browser.close();
console.log(problems ? `${problems} taşan öğe` : 'Taşma yok');
process.exit(problems ? 1 : 0);
```

### 10. Önbellek başlıkları

Cloudflare Pages, `public/_headers` dosyasındaki kuralları HTTP başlığı olarak uyguluyor ([Cloudflare Pages: Headers](https://developers.cloudflare.com/pages/configuration/headers/)). Adı değişmeden içeriği değişmeyecek dosyalar (fontlar, WebP varyantları, Astro'nun adında içerik özeti (hash) taşıyan dosyaları) bir yıl ve `immutable` olarak önbelleğe alınıyor; tekrar ziyarette bu dosyalar için hiç istek atılmıyor. Dosyanın ilgili kısmı:

```text
/fonts/*
  Cache-Control: public, max-age=31536000, immutable
  Access-Control-Allow-Origin: *

/_img/*
  Cache-Control: public, max-age=31536000, immutable

/_astro/*
  Cache-Control: public, max-age=31536000, immutable
```

## SEO ve GEO: hız dışında neler yapıldı?

Hızlı bir sayfa bulunamıyorsa işe yaramaz. Aynı taşımada şunlar da yapıldı:

- **Yapısal veri:** Her sayfada Person, Organization ve WebSite düğümlerinden oluşan tek bir JSON-LD grafı var; sayfaya özel düğümler (BlogPosting, ProfilePage vb.) bunlara `@id` ile bağlanıyor. Arama motorlarının ve yapay zekâ asistanlarının kişiyi ve şirketi tek bir varlık olarak eşleştirmesi kolaylaşıyor.
- **hreflang:** Türkçe ve İngilizce sayfalar birbirini `tr`, `en` ve `x-default` ile gösteriyor.
- **llms.txt ve llms-full.txt:** [llmstxt.org](https://llmstxt.org/) önerisine göre biçimlenmiş, kimliği ve tüm sayfaları listeleyen bir dosya. `llms-full.txt` tüm yazıların düz metnini tek dosyada veriyor; bu ikincisi önerinin parçası değil, yaygınlaşan bir gelenek. İkisi de derlemede içerikten üretiliyor.
- **IndexNow:** Her yayından sonra site haritasındaki adresler IndexNow'a bildiriliyor. Bing ve Yandex gibi arama motorları bu protokolü kullanıyor; Google kullanmıyor, onun için Search Console'a gönderilen site haritası yeterli.
- **301 yönlendirmeleri:** Eski WordPress adresleri (kökteki yazı adresleri, `/category/`, `/feed/`, `/portfolios/`) `public/_redirects` ile yeni adreslere kalıcı olarak yönleniyor. Dosya her derlemeden önce bir betikle üretiliyor; yeni yazı eklendiğinde unutulmuyor.

`llms.txt` bir Astro uç noktası olarak üretiliyor (kısaltılmış):

```typescript
// src/pages/llms.txt.ts — derlemede /llms.txt dosyasına yazılır
import site from '../data/site.json';
import { allPosts } from '../lib/blog';
import { iso } from '../lib/util';

export async function GET() {
  const posts = await allPosts('tr');
  const u = (p: string) => `${site.url}${p}`;
  const lines = [
    `# ${site.name}`,
    '',
    `> ${site.description}`,
    '',
    '## Blog yazıları (Türkçe) / Blog posts (Turkish)',
    '',
    ...posts.map((p) => `- [${p.data.title}](${u(`/blog/${p.id}/`)}) – ${iso(p.data.date)}${p.data.description ? `: ${p.data.description}` : ''}`),
    '',
  ];
  return new Response(lines.join('\n'), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}
```

## Sonuç: önce ve sonra

Mobil Lighthouse, canlı site, simüle 4G:

| Sayfa               | Performans | LCP              | Sayfa ağırlığı      |
| ------------------- | ---------- | ---------------- | ------------------- |
| Ana sayfa (TR)      | 62 → 98    | 16,8 sn → 1,9 sn | 2.975 KiB → 309 KiB |
| Hakkımda            | 67 → 93    | 9,0 sn → 2,6 sn  | 1.278 KiB → 224 KiB |
| Blog yazısı         | 70 → 96    | 5,7 sn → 2,4 sn  | 1.580 KiB → 298 KiB |
| İngilizce ana sayfa | 67 → 93    | 13,7 sn → 2,7 sn | —                   |

Masaüstünde tüm sayfalar 99–100 aldı. Erişilebilirlik, En İyi Uygulamalar ve SEO kategorileri 100, CLS 0. Ana sayfanın ağırlığı yaklaşık onda birine indi.

Dürüst not: Hakkımda ve İngilizce ana sayfada laboratuvar LCP'si 2,5 saniyelik eşiğin biraz üzerinde; iyileştirme payı hâlâ var. Benim çıkardığım ders şu: çerçeve değişikliği zemini hazırladı, farkı ise LCP görselini erken keşfettirmek ve gereksiz baytları hiç göndermemek yarattı. Aynı "önce ölç, sonra tek değişken değiştir" yaklaşımını veri tabanı tarafında [SQL Server performans ayarı](https://www.abdulazizakyol.com/blog/sql-server-performans-ayari-indeks-istatistik-ve-sorgu-plani/) yazısında anlattım.

## Kendi sitenizde uygulamak için kontrol listesi

1. **Önce ölçün:** Lighthouse'u mobil profilde birkaç kez çalıştırıp aralığı not edin; varsa Search Console'daki Core Web Vitals raporuna (gerçek kullanıcı verisi) bakın.
2. **LCP öğesini bulun.** Görselse: `<img>`, `fetchpriority="high"`, lazy yok, `srcset` + `sizes`, gerekirse `imagesrcset`'li preload.
3. **Her görseli birden çok genişlikte WebP'ye çevirin** ve `width`/`height` verin.
4. **Ekranın altındaki görsellere** `loading="lazy"` ve `decoding="async"` ekleyin.
5. **Fontları kendi alan adınızdan woff2 olarak sunun,** gereken alt kümeleri seçin, ilk ekrandaki bir iki fontu `crossorigin` ile preload edin.
6. **Render'ı engelleyen CSS'i azaltın;** küçükse sayfaya gömün.
7. **Üçüncü taraf gömmeleri** (video, harita, sohbet penceresi) tıklanınca yükleyin.
8. **CDN'in eklediği betikleri** (e-posta gizleme, analiz) gözden geçirin.
9. **Mobil taşmayı otomatik test edin;** kontrastı ve küçük yazıları düzeltin.
10. **Değişmeyen dosyalara** uzun ve `immutable` önbellek başlığı verin.
11. **Taşımada eski adresleri 301 ile yönlendirin;** yapısal veri, hreflang, site haritası ve llms.txt'yi derlemede otomatik üretin.

## Sık sorulan sorular

### WordPress'ten Astro'ya geçmek site hızını neden artırır?

Astro sayfaları derleme sırasında düz HTML'e çevirir ve sunucuda her istekte PHP ya da veritabanı çalışmaz; CDN sayfanın tamamını önbellekten sunabilir. Ancak asıl kazanç görseller, fontlar ve üçüncü taraf betikler gibi ağır kaynakların tek tek ele alınmasından gelir. Çerçeve değişikliği zemini hazırlar, tek başına yetmez.

### LCP nedir ve iyi bir LCP değeri kaç saniyedir?

Largest Contentful Paint (LCP), sayfadaki en büyük görselin ya da metin bloğunun ekrana çizildiği andır. web.dev'e göre gerçek ziyaretlerin 75. yüzdelik diliminde 2,5 saniye ya da altı iyi kabul edilir. Lighthouse laboratuvarda simüle bir ölçüm yapar; gerçek kullanıcı verisi ayrıca izlenmelidir.

### Lighthouse puanı neden her ölçümde farklı çıkıyor?

Ağ yönlendirmesi, sunucu yanıt süresi, ölçüm yapılan makine ve tarayıcı eklentileri gibi dış etkenler sonucu değiştirir. Bu sitenin ana sayfası ölçümler arasında 87 ile 98 arasında dalgalandı. Tek bir sayıya değil, birkaç ölçümün aralığına bakmak daha doğrudur.

### Astro'da public klasöründeki görseller neden otomatik optimize edilmiyor?

Astro, public/ altındaki dosyaları işlemeden olduğu gibi kopyalar; Image ve Picture bileşenleri src/ altındaki görseller içindir. Eski WordPress adreslerini korumak için görselleri public/uploads altında bıraktık ve WebP varyantlarını derleme sonunda çalışan küçük bir entegrasyonla sharp kullanarak ürettik.

### llms.txt nedir, ne işe yarar?

llms.txt, sitenin kök dizininde duran ve yapay zekâ asistanlarına siteyi Markdown biçiminde özetleyen bir dosyadır: başlık, kısa özet ve önemli sayfaların bağlantı listeleri. llmstxt.org'daki bir öneridir, resmî bir standart ya da tanımlı bir sıralama faktörü değildir; asistanların siteyi doğru anlamasını kolaylaştırır.

## Kaynaklar

1. [Web Vitals (web.dev)](https://web.dev/articles/vitals)
2. [Optimize Largest Contentful Paint (web.dev)](https://web.dev/articles/optimize-lcp)
3. [Lighthouse performance scoring (Chrome for Developers)](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring)
4. [Astro Configuration Reference](https://docs.astro.build/en/reference/configuration-reference/)
5. [Astro Integration API](https://docs.astro.build/en/reference/integrations-reference/)
6. [Cloudflare Pages: Headers](https://developers.cloudflare.com/pages/configuration/headers/)

---

Abdulaziz Akyol, CX Teknoloji'nin (yapay zekâ, görüntü işleme ve IoT) kurucusudur. Asıl sürüm: https://www.abdulazizakyol.com/blog/astro-ve-cloudflare-ile-hizli-kisisel-site-core-web-vitals/
