Cloudflare Pages ile Static Site ve SPA Deployment Rehberi
Cloudflare Pages’i ilk keşfettiğimde “bu da ne kadar basit” diye düşünmüştüm. Sonra altındaki gücü görünce fikrimi değiştirdim. Netlify ve Vercel’e alışmış biri olarak Cloudflare’in edge ağını Pages ile birleştirme fikri başta gereksiz bir karmaşıklık gibi göründü, ama production’da büyük bir React SPA’yı oraya taşıdıktan sonra 40ms altında global TTFB görünce bakış açım tamamen değişti. Bu yazıda statik site ve SPA deployment sürecini, gerçek senaryolarla ve sıklıkla karşılaşılan sorunlarla birlikte ele alacağız.
Cloudflare Pages Nedir ve Neden Önemli
Cloudflare Pages, Cloudflare’in 300’den fazla PoP noktasına sahip edge ağı üzerinde çalışan bir static hosting ve full-stack deployment platformudur. GitHub veya GitLab reponuza bağlandığınızda her push’ta otomatik build ve deploy yapıyor. Ama onu diğerlerinden ayıran şey, Pages Functions aracılığıyla Workers runtime’ına doğrudan erişim sağlamasıdır. Bu sayede saf bir static site de deploy edebilir, server-side rendering ihtiyacı olan bir SPA da çalıştırabilirsiniz.
Temel avantajlar şunlar:
- Ücretsiz planda sınırsız statik site bandwidth
- Her commit için otomatik preview deployment
- Cloudflare Workers ile entegre edge functions
- KV, D1, R2 ve Durable Objects’e doğrudan erişim
- Zero-config SSL ve custom domain desteği
Wrangler CLI ile Başlangıç
Cloudflare Pages’i yönetmenin en pratik yolu Wrangler CLI’dır. Önce kuralım:
npm install -g wrangler
# Versiyon kontrolü
wrangler --version
# Cloudflare hesabınıza login olun
wrangler login
# Bu komut browser açarak OAuth flow başlatır
# Token ~/.wrangler/config/default.toml'a kaydedilir
Login sonrası hesap bilgilerinizi doğrulamak için:
wrangler whoami
# Çıktı örneği:
# Getting User settings...
# 👋 You are logged in with an API Token, associated with the email: [email protected]
# Account Name: Sirketiniz
# Account ID: abc123def456...
İlk Static Site Deployment
En basit senaryodan başlayalım. Elimizde hazır bir static site var, bunu Pages’e atalım.
# Proje dizinine gidin
cd /var/www/statik-site
# Build klasörünüzü doğrudan deploy edin
wrangler pages deploy ./dist --project-name="sirket-website"
# Eğer proje yoksa otomatik oluşturur
# Deployment URL'ini konsolda göreceksiniz:
# ✨ Success! Your site is now available at https://sirket-website.pages.dev
Eğer projeyi CLI yerine GitHub’a bağlamak istiyorsanız, Cloudflare Dashboard üzerinden “Pages > Create a project > Connect to Git” adımlarını izliyorsunuz. Ama ben CI/CD pipeline’larında çalışırken Wrangler’ı tercih ediyorum çünkü daha fazla kontrol sağlıyor.
React SPA için wrangler.toml Konfigürasyonu
SPA deployment’larında işler biraz farklılaşıyor. Özellikle client-side routing kullanan uygulamalarda 404 sorunuyla karşılaşmamak için doğru konfigürasyon şart. Proje kökünde wrangler.toml oluşturun:
cat > wrangler.toml << 'EOF'
name = "react-uygulamamiz"
compatibility_date = "2024-01-01"
pages_build_output_dir = "./build"
[vars]
ENVIRONMENT = "production"
API_BASE_URL = "https://api.sirketiniz.com"
[[kv_namespaces]]
binding = "CACHE"
id = "kv_namespace_id_buraya"
[[d1_databases]]
binding = "DB"
database_name = "uygulama-db"
database_id = "d1_database_id_buraya"
EOF
Dikkat etmeniz gereken nokta şu: pages_build_output_dir alanı Workers’taki main alanından farklıdır. Pages için bu alanı kullanmanız gerekiyor, yoksa deploy hata verir.
SPA Routing Sorununu Çözmek
React Router veya Vue Router kullanan uygulamalarda kullanıcı direkt olarak https://uygulamaniz.com/dashboard/settings adresine gittiğinde 404 alır. Bunun sebebi sunucunun dashboard/settings path’ini bilmemesidir. Cloudflare Pages’te bunu çözmenin yolu _redirects dosyasıdır:
# public/_redirects dosyası oluşturun
cat > public/_redirects << 'EOF'
# SPA fallback - tüm route'ları index.html'e yönlendir
/* /index.html 200
# API çağrılarını backend'e yönlendir
/api/* https://api.sirketiniz.com/:splat 200
# Eski URL'leri yeni yapıya yönlendir
/eski-sayfa /yeni-sayfa 301
/blog/eski-post /makaleler/eski-post 301
EOF
Alternatif olarak _headers dosyasıyla cache ve güvenlik başlıkları da ekleyebilirsiniz:
cat > public/_headers << 'EOF'
# Tüm sayfalar için güvenlik başlıkları
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
# Statik assetler için agresif cache
/static/*
Cache-Control: public, max-age=31536000, immutable
# API yanıtları için cache yok
/api/*
Cache-Control: no-store, no-cache
EOF
Pages Functions ile Edge API Yazma
İşte Cloudflare Pages’in gerçek gücü burada ortaya çıkıyor. functions/ dizininde oluşturduğunuz her dosya otomatik olarak bir API endpoint’i oluyor. Bu file-based routing sistemi Next.js’e benziyor ama Workers runtime üzerinde çalışıyor.
mkdir -p functions/api
cat > functions/api/kullanici.js << 'EOF'
// GET /api/kullanici endpoint'i
export async function onRequestGet(context) {
const { request, env, params } = context;
// Authorization header kontrolü
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response(JSON.stringify({
hata: 'Yetkisiz erişim'
}), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
const token = authHeader.split(' ')[1];
// D1 database'den kullanıcı çek
try {
const kullanici = await env.DB.prepare(
'SELECT id, email, ad, soyad FROM kullanicilar WHERE token = ?'
).bind(token).first();
if (!kullanici) {
return new Response(JSON.stringify({
hata: 'Kullanıcı bulunamadı'
}), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify(kullanici), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'private, max-age=300'
}
});
} catch (hata) {
console.error('DB hatası:', hata.message);
return new Response(JSON.stringify({
hata: 'Sunucu hatası'
}), { status: 500 });
}
}
// POST /api/kullanici endpoint'i
export async function onRequestPost(context) {
const { request, env } = context;
const veri = await request.json();
const { email, ad, soyad, sifre } = veri;
if (!email || !ad || !sifre) {
return new Response(JSON.stringify({
hata: 'Zorunlu alanlar eksik'
}), { status: 400 });
}
// Şifre hash'leme (gerçek senaryoda bcrypt benzeri kullanın)
const sifreHash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(sifre)
);
const sifreHex = Array.from(new Uint8Array(sifreHash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
await env.DB.prepare(
'INSERT INTO kullanicilar (email, ad, soyad, sifre_hash) VALUES (?, ?, ?, ?)'
).bind(email, ad, soyad, sifreHex).run();
return new Response(JSON.stringify({
mesaj: 'Kullanıcı oluşturuldu'
}), { status: 201 });
}
EOF
Middleware ile Global Request Handling
functions/_middleware.js dosyası tüm istekler için çalışan bir interceptor görevi görür. Rate limiting, logging, CORS gibi cross-cutting concerns buraya taşınır:
cat > functions/_middleware.js << 'EOF'
// Rate limiting için KV kullanıyoruz
export async function onRequest(context) {
const { request, env, next } = context;
const ip = request.headers.get('CF-Connecting-IP');
const yol = new URL(request.url).pathname;
// Sadece API route'larına rate limiting uygula
if (yol.startsWith('/api/')) {
const anahtarAdi = `rate_limit:${ip}`;
const mevcut = await env.CACHE.get(anahtarAdi);
const istek_sayisi = mevcut ? parseInt(mevcut) : 0;
// Dakikada 60 istek limiti
if (istek_sayisi >= 60) {
return new Response(JSON.stringify({
hata: 'Rate limit aşıldı. 1 dakika bekleyin.'
}), {
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': '60',
'X-RateLimit-Limit': '60',
'X-RateLimit-Remaining': '0'
}
});
}
// Sayacı artır, 60 saniye TTL
await env.CACHE.put(anahtarAdi, String(istek_sayisi + 1), {
expirationTtl: 60
});
// CORS başlıkları
const yanit = await next();
const yeniYanit = new Response(yanit.body, yanit);
yeniYanit.headers.set('Access-Control-Allow-Origin',
env.ENVIRONMENT === 'production'
? 'https://sirketiniz.com'
: '*'
);
yeniYanit.headers.set('Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, OPTIONS'
);
yeniYanit.headers.set('X-RateLimit-Remaining',
String(60 - istek_sayisi - 1)
);
return yeniYanit;
}
return next();
}
EOF
CI/CD Pipeline Entegrasyonu
GitHub Actions ile otomatik deployment kurmak production ortamları için çok önemli. Şu yapıyı kullanıyorum:
# .github/workflows/deploy.yml
cat > .github/workflows/deploy.yml << 'EOF'
name: Cloudflare Pages Deploy
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Kodu çek
uses: actions/checkout@v4
- name: Node.js kur
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Bağımlılıkları yükle
run: npm ci
- name: Test çalıştır
run: npm run test -- --coverage --passWithNoTests
- name: Build al
run: npm run build
env:
REACT_APP_API_URL: ${{ secrets.API_URL }}
REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
- name: Production deploy (main branch)
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
npx wrangler pages deploy ./build
--project-name="sirket-website"
--branch="main"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Preview deploy (PR için)
if: github.event_name == 'pull_request'
run: |
DEPLOY_URL=$(npx wrangler pages deploy ./build
--project-name="sirket-website"
--branch="pr-${{ github.event.pull_request.number }}"
2>&1 | grep "pages.dev" | tail -1)
echo "Preview URL: $DEPLOY_URL"
echo "PREVIEW_URL=$DEPLOY_URL" >> $GITHUB_ENV
- name: PR'a yorum ekle
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 Preview deployment hazır!nn${process.env.PREVIEW_URL}`
})
EOF
Ortam Değişkenleri ve Secret Yönetimi
Secrets’ı wrangler ile yönetmek production’da hayat kurtarır. Dashboard yerine CLI’ı kullanmanızı öneririm çünkü scriptlenebilir:
# Secret ekle (interaktif, değeri terminal'de göstermez)
wrangler pages secret put DATABASE_URL --project-name="sirket-website"
# Toplu secret yükleme (CI/CD için)
echo "STRIPE_SECRET_KEY=sk_live_xxx" | wrangler pages secret put STRIPE_SECRET_KEY
--project-name="sirket-website"
# Mevcut secret listesi (değerleri göstermez)
wrangler pages secret list --project-name="sirket-website"
# Secret sil
wrangler pages secret delete ESKI_SECRET --project-name="sirket-website"
# Environment'a özgü secret (production vs preview)
wrangler pages secret put API_KEY
--project-name="sirket-website"
--env production
Önemli bir not: wrangler.toml içindeki [vars] alanı plaintext değerler içindir, production secret’ları buraya koymayın. Bunlar repo’ya commit edilir ve herkese açık olur.
Deployment Sorun Giderme
Production’da karşılaştığım tipik sorunlar ve çözümleri:
Build hatası – Node version uyumsuzluğu:
# Pages dashboard'da build komutunu şu şekilde ayarlayın
# Build command alanına:
node --version && npm ci && npm run build
# Ya da package.json engines alanı ekleyin
cat > package.json_ek << 'EOF'
{
"engines": {
"node": ">=20.0.0"
}
}
EOF
# Cloudflare Pages otomatik NODE_VERSION env değişkenini okur
# Dashboard > Settings > Environment variables'a ekleyin:
# NODE_VERSION = 20
Function boyut limiti aşımı:
# Mevcut bundle boyutunu kontrol et
wrangler pages functions build --outdir=./dist_functions
# Boyutu gör
du -sh ./dist_functions/*
# Eğer 1MB limiti aşıyorsa dinamik import kullanın
# functions/agir-islem.js içinde:
# const agirModul = await import('../src/agir-kutuphane.js');
Preview deployment’larda environment variable eksikliği:
# Preview ve production için ayrı ayrı secret tanımlayın
wrangler pages secret put API_URL
--project-name="sirket-website"
--env preview
wrangler pages secret put API_URL
--project-name="sirket-website"
--env production
# Deployment listesi ve durumlarını kontrol et
wrangler pages deployment list --project-name="sirket-website"
# Belirli bir deployment'ın loglarını çek
wrangler pages deployment tail
--project-name="sirket-website"
deployment-id-buraya
Performans Optimizasyonu
Edge’de çalışmanın avantajını tam kullanmak için cache stratejisi önemli. Özellikle API yanıtlarını Cache API ile edge’de cache’lemek ciddi fark yaratıyor:
cat > functions/api/urunler.js << 'EOF'
export async function onRequestGet(context) {
const { request, env } = context;
const url = new URL(request.url);
// Cache key oluştur
const cacheKey = new Request(url.toString(), request);
const cache = caches.default;
// Önce cache'e bak
let yanit = await cache.match(cacheKey);
if (yanit) {
// Cache hit - header ekle
const yeniYanit = new Response(yanit.body, yanit);
yeniYanit.headers.set('X-Cache', 'HIT');
return yeniYanit;
}
// Cache miss - D1'den çek
const urunler = await env.DB.prepare(
'SELECT * FROM urunler WHERE aktif = 1 ORDER BY siralama LIMIT 50'
).all();
yanit = new Response(JSON.stringify(urunler.results), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300, stale-while-revalidate=60',
'X-Cache': 'MISS'
}
});
// Edge'de 5 dakika cache'le
context.waitUntil(cache.put(cacheKey, yanit.clone()));
return yanit;
}
EOF
context.waitUntil() burada kritik. Yanıtı kullanıcıya döndürdükten sonra cache yazma işlemini arka planda tamamlıyor. Kullanıcı cache işlemini beklemek zorunda kalmıyor.
Rollback ve Deployment Yönetimi
Production’da bir şeyler ters giderse hızlı rollback şarttır:
# Tüm deployment'ları listele
wrangler pages deployment list --project-name="sirket-website" --env production
# Belirli bir deployment'a rollback yap
wrangler pages deployment rollback
--project-name="sirket-website"
--env production
ESKI_DEPLOYMENT_ID
# Anlık deployment durumunu izle
wrangler pages deployment tail
--project-name="sirket-website"
--env production
# Custom domain'i farklı deployment'a geçici yönlendirme
# Dashboard > Custom Domains > Manage > Point to specific deployment
Production ortamında deployment’larınızı etiketleyip tutmak iyi bir pratiktir. Ben her release öncesi deployment ID’sini bir not dosyasına veya Git tag’ine yazıyorum. Gece 2’de acil rollback yaparken deployment ID arayıp bulmak istemezsiniz.
Sonuç
Cloudflare Pages ilk bakışta basit bir static hosting servisi gibi görünse de altında Workers runtime, KV, D1 ve R2 entegrasyonuyla çok daha kapsamlı bir platform. Statik siteler için sıfır konfigürasyonla başlayıp ihtiyaç büyüdükçe edge functions, middleware ve database entegrasyonlarıyla genişletebilirsiniz.
Benim için en değerli özellikler şunlar oldu: Her PR için otomatik preview deployment, bu sayede QA ekibi her özelliği merge öncesi gerçek ortamda test edebiliyor. Edge’deki cache API entegrasyonu sayesinde database sorgularını kullanıcıya yakın PoP noktalarında cache’leyip dramatik gecikme iyileştirmeleri elde ettik. Ve tabii ki ücretsiz plandaki bandwidth sınırsızlığı, trafik ani artışlarında ekstra ücret ödeme kaygısını ortadan kaldırıyor.
Eğer şu an Netlify veya Vercel kullanıyorsanız ve Cloudflare DNS’i zaten altyapınızda varsa, geçiş yapmak için gerçek bir engel yok. wrangler.toml ve _redirects dosyalarını anlayan birisi için migration birkaç saati geçmiyor. Mevcut projelerinizi taşımadan önce küçük bir test projesiyle başlayın, platform davranışını tanıyın, sonra production’a alın.
