Tüm yazılar
Veri Otomasyonu 7 dk okuma

Cloudflare Pages ile ücretsiz ve hızlı statik site yayını

Cloudflare Pages ile statik siteyi wrangler üzerinden yayınlama: özel alan adı, _headers ve _redirects, önizleme-üretim dalı ayrımı ve yayın sonrası doğrulama.

#cloudflare

Statik bir siteyi yayınlamak için hâlâ aylık ücretli bir sunucu tutmak, bana göre çoğu projede gereksiz bir masraf ve bakım yükü. Kendi sitelerimin bir kısmını bir süre önce klasik bir sunucudan Cloudflare Pages üzerine taşıdım; HTML, CSS ve JavaScript'ten oluşan bir siteyi ücretsiz katmanda, küresel bir dağıtım ağından ve otomatik TLS sertifikasıyla sunmak için Pages şu an bildiğim en az sürtünmeli yol. Bu yazıda bir statik siteyi komut satırından Pages'e nasıl dağıttığımı, özel alan adını nasıl bağladığımı, _headers ve _redirects dosyalarıyla neleri çözdüğümü ve önizleme ile üretim dağıtımlarını ayırırken düştüğüm tuzağı anlatacağım.

"Statik site" derken kastım, sunucuda çalışan bir uygulama kodu olmadan doğrudan dosya olarak sunulabilen her şey: elle yazılmış HTML sayfaları, Hugo, Astro, Eleventy gibi üreteçlerin çıktısı ya da bir React veya Vue uygulamasının derlenmiş hâli. Pages'in bir de sunucu tarafında çalışan fonksiyon katmanı var, ama o bu yazının kapsamı dışında; burada tamamen dosya dağıtımına odaklanıyorum.

Baştan söyleyeyim: burada anlattığım her şeyi kendi otomasyon hattımda kullanıyorum ve en önemli dersim teknik değil, yöntemle ilgili. Dağıtım aracı "başarılı" dediğinde iş bitmiş sayılmıyor. Yayın sonrası doğrulamayı neden vazgeçilmez bir adım olarak gördüğümü son bölümde somut bir örnekle göstereceğim.

Wrangler ile komut satırından yayın

Pages'e iki şekilde dağıtım yapılabilir: depoyu GitHub veya GitLab'a bağlayıp her push'ta Cloudflare'in derlemesini beklemek, ya da derlenmiş klasörü doğrudan yüklemek. Ben ikinci yolu tercih ediyorum; derlemeyi kendi makinemde ya da kendi sunucumda yapıyorum, Pages'e yalnızca sonuç gidiyor. Bunun aracı Cloudflare'in komut satırı programı wrangler. Dikkat edilmesi gereken bir nokta: bir proje ya Git bağlantılı ya da doğrudan yükleme tabanlı olarak açılıyor; ikisini aynı projede karıştırmak mümkün değil, o yüzden kararı proje oluştururken vermek gerekiyor.

Kurulum ve ilk dağıtım şöyle görünüyor:

# Node.js kurulu bir makinede, proje dizininde
npm install --save-dev wrangler

# Tarayıcı üzerinden Cloudflare hesabına yetki ver
npx wrangler login

# Projeyi bir kez oluştur; üretim dalını açıkça belirt
npx wrangler pages project create ornek-site --production-branch=main

# Derleme damgasını index.html'e yaz, sonra dist/ klasörünü dağıt
DAMGA=$(git rev-parse --short HEAD)
sed -i "s/__DAMGA__/$DAMGA/" dist/index.html
npx wrangler pages deploy dist --project-name=ornek-site --branch=main
echo "$DAMGA" > son-dagitim.txt

Otomasyon senaryosunda, yani bir sunucudaki zamanlanmış işten ya da CI hattından dağıtım yapılacaksa wrangler login kullanılamaz; onun yerine CLOUDFLARE_API_TOKEN ve CLOUDFLARE_ACCOUNT_ID ortam değişkenleri tanımlanır. Token'ı yalnızca Pages düzenleme yetkisiyle sınırlı oluşturmak ve düz metin olarak hiçbir betiğe gömmemek gerekiyor; ben bu tür sırları şifreli bir kasada tutup komuta yalnızca çalışma anında ortam değişkeni olarak veriyorum.

Komut satırında dikkat ettiğim birkaç nokta:

  • wrangler pages publish eski addır; güncel sürümlerde komut pages deploy. Eski rehberlerden kopyalanan betikler burada takılıyor.
  • Dağıtılan klasör derleme çıktısı olmalı, kaynak dizin değil. dist yerine yanlışlıkla proje kökünü vermek node_modules dahil her şeyi yüklemeye çalışır.
  • Çalışma dizini bir Git deposuysa wrangler dal adını ve commit'i otomatik okur; değilse --branch vermek zorunludur. Bu ayrıntı, aşağıda anlatacağım tuzağın kaynağı.

Özel alan adı bağlama

Her proje proje-adi.pages.dev altında yayına girer, ama gerçek bir sitede kendi alan adınızı istersiniz. Alan adı zaten Cloudflare DNS'te yönetiliyorsa iş birkaç dakikadır: Pages projesinin özel alan adları bölümünden alan adını eklersiniz, Cloudflare gerekli CNAME kaydını sizin adınıza oluşturur ve sertifikayı kendisi çıkarır. Alan adı başka bir sağlayıcıdaysa aynı CNAME'i oraya elle girmeniz gerekiyor; bu durumda doğrulama daha uzun sürebiliyor. Kök alan adı ile www ön ekini ayrı ayrı eklemek ve birini diğerine yönlendirmek de size kalıyor; Pages ikisini kendiliğinden eşitlemiyor.

Burada öğrendiğim bir ders var: alan adı panelde beklemede görünürken "site yayına girdi" demeyin. Bir keresinde eski sunucuyu kapatmayı planlarken alan adının hâlâ doğrulanmadığını fark ettim. O günden beri iki yoldan test ediyorum: hem pages.dev adresinden hem özel alan adından aynı içerik geliyorsa kabul ediyorum, eski sunucuyu da bir süre daha kapatmıyorum. Geri dönüş yolu açık kaldığı sürece taşınma stressiz oluyor.

_headers ve _redirects dosyaları

Statik sitede sunucu yapılandırması olmadığı için HTTP başlıkları ve yönlendirmeler nereye yazılacak sorusu doğuyor. Pages bunun için dağıtılan klasörün köküne konan iki düz metin dosyasını okuyor. _headers dosyası yol desenine göre yanıt başlığı ekler; ben güvenlik başlıklarını ve önbellek sürelerini burada tutuyorum:

/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()

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

/*.html
  Cache-Control: public, max-age=0, must-revalidate

Buradaki mantık basit: dosya adında karma (hash) taşıyan varlıklar bir yıl önbelleklenebilir, çünkü içerik değişince adı da değişir; HTML sayfaları ise her istekte yeniden doğrulanmalı, yoksa ziyaretçi eski sayfayı görmeye devam eder. _redirects dosyası da aynı yol deseni mantığıyla çalışır:

# eski adres        yeni adres                    durum kodu
/eski-yazi          /blog/yeni-yazi               301
/blog/*             /articles/:splat              301
/indir              https://ornek.com/dosya.pdf   302

# tek sayfa uygulaması: eşleşmeyen her yolu index.html ile karşıla
/*                  /index.html                   200

İki dosyada da dikkat ettiğim şeyler:

  • Girinti: _headers içinde başlık satırları yol satırının altında boşlukla girintili olmak zorunda. Girintiyi unutunca dosya sessizce yok sayılıyor, hata almıyorsunuz.
  • Sıra: _redirects içinde ilk eşleşen kural kazanır. Genel /* kuralını en sona koymazsanız özel kurallarınız hiç çalışmaz.
  • Sınırlar: Cloudflare belgelerinde kural sayısı için üst sınırlar tanımlı; büyük bir siteyi taşırken yönlendirme listesini önce o sınırlara göre gözden geçirmek gerekiyor.
  • Bu dosyalar derleme çıktısında olmalı. Kaynakta duran ama dist klasörüne kopyalanmayan bir _headers hiçbir şey yapmaz; üreteçlerin çoğunda public veya static klasörüne koymak yeterli.

Önizleme ile üretim dalı ayrımı

Pages'in en sevdiğim özelliği şu: üretim dalı dışındaki her dağıtım ayrı bir önizleme adresi alır. main üretim dalıysa, --branch=deneme ile yaptığınız dağıtım deneme.ornek-site.pages.dev gibi bir adreste yayına girer, özel alan adına dokunmaz. Bir tasarım değişikliğini göstermek ya da yeni bir yönlendirme kuralını canlıya çıkmadan denemek için ideal.

Tuzak da tam burada. Wrangler dal adını Git'ten okuduğu için, üretim dalı main olan bir projede yerelde master ya da bir özellik dalındayken pages deploy çalıştırırsanız dağıtım başarıyla tamamlanır, yeşil bir başarı mesajı ve bir URL alırsınız; ama o URL bir önizlemedir. Özel alan adınız eski içeriği sunmaya devam eder. Bunu bizzat yaşadım: otomasyon günlüğüne "yayınlandı" diye yazılmıştı, site ise değişmemişti. Aracın çıktısındaki adresin bir önizleme alt alanı mı yoksa üretim mi olduğuna bakmadan bunu fark edemezsiniz. O günden beri betiklerimde --branch parametresini hep açıkça veriyorum; Git'in o an hangi dalda olduğuna güvenmiyorum. Proje adlarını da ortama göre ayırıyorum; bu, Compose projelerinde ad çakışmasını önlemek için uyguladığım kuralın aynısı: varsayılan ada güvenme, adı sen ver.

Yayın sonrası doğrulama neden şart

Yukarıdaki olayın bana öğrettiği kural: dağıtım aracının başarılı demesi, ziyaretçinin doğru içeriği gördüğü anlamına gelmez. Araç kendi işini doğru yapmıştı; yanlış olan benim ona verdiğim hedefti. Bu yüzden her dağıtımın sonuna, üretim adresini dışarıdan bir istemci gibi kontrol eden küçük bir betik ekledim. Betik üç şeye bakıyor: sayfa 200 dönüyor mu, beklediğim güvenlik başlıkları geliyor mu ve sayfada bu derlemeye özgü bir damga var mı. Damga için derleme sırasında index.html içine satırını koyuyorum; yukarıdaki dağıtım betiğindeki sed komutu yer tutucuyu commit kısaltmasıyla değiştiriyor.

import sys
import urllib.error
import urllib.request

SITE = "https://ornek.com"
BEKLENEN_BASLIKLAR = {
    "x-content-type-options": "nosniff",
    "x-frame-options": "DENY",
}

BASLIKLAR = {
    "Cache-Control": "no-cache",
    "User-Agent": "dagitim-dogrulama/1.0",
}

def kontrol(yol, damga):
    istek = urllib.request.Request(SITE + yol, headers=BASLIKLAR)
    try:
        with urllib.request.urlopen(istek, timeout=15) as yanit:
            durum = yanit.status
            basliklar = yanit.headers
            govde = yanit.read().decode("utf-8", errors="replace")
    except urllib.error.HTTPError as hata:
        return [f"durum {hata.code}"]
    except urllib.error.URLError as hata:
        return [f"baglanti hatasi: {hata.reason}"]

    hatalar = []
    if not (200 <= durum < 300):
        hatalar.append(f"durum {durum}")
    for ad, deger in BEKLENEN_BASLIKLAR.items():
        if basliklar.get(ad, "").lower() != deger.lower():
            hatalar.append(f"baslik eksik: {ad}")
    if damga not in govde:
        hatalar.append(f"damga yok: {damga}")
    return hatalar

if __name__ == "__main__":
    damga = sys.argv[1]          # dagitim betiginin yazdigi son-dagitim.txt icerigi
    sorunlu = {}
    for yol in ["/", "/blog/", "/iletisim/"]:
        bulgular = kontrol(yol, damga)
        if bulgular:
            sorunlu[yol] = bulgular
    if sorunlu:
        for yol, bulgular in sorunlu.items():
            print(f"HATA {yol}: {', '.join(bulgular)}")
        sys.exit(1)
    print("dogrulama gecti:", damga)

Betik sıfır dışında bir kodla çıkarsa dağıtım işi de başarısız sayılıyor ve bana bildirim düşüyor. Çıkış kodunu bildirime bağlamak için sistem izleme betiğimde kullandığım düzenin aynısını kullanıyorum: sonuç bir dosyaya yazılır, ayrı bir görev o dosyayı okur ve karar verir. Aynı betiği hem pages.dev adresine hem özel alan adına çalıştırmak, alan adı bağlantısı koptuğunda da yakalıyor.

Doğrulamada önemsediğim birkaç ayrıntı:

  • Önbellek: tarayıcı ya da aradaki bir önbellek eski HTML'i bir süre daha sunabilir. İsteğe Cache-Control: no-cache başlığı eklemek çoğu durumda yetiyor.
  • Kimlik: Python'un varsayılan User-Agent değeriyle gelen istekleri Cloudflare'in bot koruması 403 ile reddedebiliyor; kendi sitemde tam bunu yaşadım. Betiğe tanımlayıcı bir User-Agent vermek yetti, ama koruma daha sıkıysa doğrulama istemcisi için ayrı bir WAF istisnası gerekebilir.
  • Damga olmadan içerik kontrolü yanıltıcı: "sayfa açılıyor" testi eski sürümde de geçer. Derlemeye özgü bir iz şart.
  • Tek denemede alarm üretmiyorum; dağıtımın yayılması birkaç saniye sürebiliyor, ikinci deneme kısa bir aradan sonra yapılıyor. İki deneme de düşerse gerçek sorun var demektir.

Özetle: Cloudflare Pages, statik bir siteyi ücretsiz katmanda, hızlı ve bakım yükü olmadan yayınlamak için gördüğüm en pratik araçlardan biri. Wrangler ile dağıtım tek komut, alan adı bağlama birkaç dakika, başlık ve yönlendirme yönetimi iki düz metin dosyası. Asıl mesele, aracın başarı mesajını değil, ziyaretçinin gördüğünü kanıt saymak. Kendi siteniz ya da müşterileriniz için bu tür bir yayın ve doğrulama hattı kurmak istiyorsanız, veri ve otomasyon danışmanlığı yaptığım ana sayfadaki /#contact bölümünden bana yazabilirsiniz.