Modern Bir SaaS Geliştirmek: Next.js 14 ve AI ile CV Platformu Yolculuğumuz (Teknik Vaka Analizi)
6 ay önce bir kahve sohbetinde arkadaşım "CV hazırlama sitesi yapmayı düşünüyorum" dedi. Ben de "Pazarda zaten 50 tane var, farkın ne olacak?" diye sordum.
"Kullanıcı verilerini sunucumda saklamayacağım" dedi.
"İmkansız" dedim. "O zaman nasıl çalışacak?"
Bugün cvhazirla.app production'da, binlerce kullanıcı verilerini bizim sunucularımıza göndermeden CV hazırlıyor. Bu makale o "imkansız" projenin teknik hikayesi. Hemen ücretsiz ve güvenli CV'nizi oluşturun - ama önce nasıl yaptığımızı okuyun, belki siz de benzer bir şey yapmak istersiniz.
Problem: Klasik SaaS Modeli Neden Yeterli Değildi?
Standart bir CV hazırlama platformu şöyle çalışır:
Client (React/Vue)
↓
API (Node.js/Django)
↓
Database (PostgreSQL/MongoDB)
↓
User data stored on serverBu yaklaşımın sorunları:
1. Gizlilik Riski: Kullanıcının CV'si (ad, telefon, mail, iş geçmişi) sunucuda saklanıyor. Veri sızıntısı durumunda milyonlarca kullanıcı etkilenebilir.
2. KVKK/GDPR Yükü: Kişisel veri işliyorsunuz, compliance dokümantasyonu, veri silme talepleri, aydınlatma metinleri... Yasal yük çok ağır.
3. Güven Sorunu: Kullanıcı "verilerimi ne yapıyorsunuz?" diye soruyor. "Saklamıyoruz ama güvenli" demek yetmiyor. Kanıt lazım.
4. Infrastructure Maliyeti: Kullanıcı sayısı arttıkça database, backup, security... Her şey büyüyor, maliyetler uçuyor.
Biz dedik ki: "Ya sunucuda veri saklamasaydık?"
Mimari Karar: Local-First Architecture
Local-First mimari demek: Veriler birincil olarak kullanıcının cihazında saklanıyor, sunucu sadece senkronizasyon ve yedekleme için var (isteğe bağlı).
Client (Next.js 14)
↓
localStorage (Encrypted)
↓
IndexedDB (Backup)
↓
[Optional] Cloud Sync (Encrypted)Avantajlar:
✅ Kullanıcı verilerini asla görmüyoruz
✅ Offline çalışabiliyor
✅ KVKK/GDPR compliance otomatik
✅ Infrastructure maliyeti minimal
✅ Kullanıcı tam kontrolde
Dezavantajlar (ve çözümlerimiz):
❌ Tarayıcı değişince veri kaybı → Opsiyonel cloud sync ile çözüldü
❌ localStorage boyut limiti (5-10MB) → Zaten CV verileri küçük, yeterli
❌ Cross-device sync zor → Şifreli cloud backup seçeneği sunuyoruz
Teknoloji Stack Seçimi
Frontend: Next.js 14 (App Router)
Neden Next.js?
1. Server-Side Rendering (SSR): SEO kritik. CV hazırlama aramaları yapan insanlar organik trafikle geliyor. Next.js SSR/SSG sayesinde Google'da üst sıralardayız.
2. App Router: Yeni routing sistemi daha temiz, layout'lar daha organize.
3. React Server Components: Bazı component'ler server-side render ediliyor, JavaScript bundle küçük kalıyor. İlk yükleme 2.1MB'dan 890KB'a düştü.
4. Edge Functions: AI özelliklerinde Vercel Edge'i kullanıyoruz, latency düşük.
javascript
// app/cv/[id]/page.tsx
export default async function CVPage({ params }) {
// Server Component - SEO için
return (
<div>
<CVEditor id={params.id} /> {/* Client Component */}
</div>
)
}Styling: TailwindCSS
Neden Tailwind?
Hızlı prototipleme
Tutarlı design system
Tree-shaking ile CSS bundle optimizasyonu
ATS-uyumlu PDF çıktısı için inline style'lar kolayca generate ediliyor
jsx
<div className="flex flex-col gap-4 p-6 bg-white rounded-lg shadow-sm">
<input
className="border border-gray-300 rounded px-3 py-2 focus:ring-2 focus:ring-blue-500"
type="text"
/>
</div>State Management: Zustand
Neden Redux değil de Zustand?
Redux boilerplate çok fazla. Basit bir CV app için overkill. Zustand minimalist ve yeterli:
javascript
import create from 'zustand'
import { persist } from 'zustand/middleware'
const useCVStore = create(
persist(
(set) => ({
cv: {},
updateCV: (data) => set({ cv: data }),
clearCV: () => set({ cv: {} })
}),
{
name: 'cv-storage', // localStorage key
storage: createJSONStorage(() => localStorage),
}
)
)persist middleware otomatik localStorage'a yazıyor. Şifrelemeyi buraya ekliyoruz.
Data Storage: localStorage + IndexedDB
localStorage: Basit key-value storage, 5-10MB limit. CV verileri için yeterli.
IndexedDB: Daha büyük veriler için (template'ler, görsel assets). Asenkron API'si var.
Şifreleme: crypto-js kullanarak AES-256 şifreleme:
javascript
import CryptoJS from 'crypto-js'
const SECRET_KEY = generateUserSpecificKey() // Her kullanıcı için unique
export const encryptData = (data) => {
return CryptoJS.AES.encrypt(
JSON.stringify(data),
SECRET_KEY
).toString()
}
export const decryptData = (encryptedData) => {
const bytes = CryptoJS.AES.decrypt(encryptedData, SECRET_KEY)
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
}Veri localStorage'a gitmeden önce şifreleniyor. Tarayıcıda bile düz metin yok.
AI Integration: Google Gemini API
Neden ChatGPT değil de Gemini?
1. Maliyet: Gemini daha ucuz. Kullanıcı başına maliyet önemli.
2. Latency: Gemini Flash modeli hızlı yanıt veriyor, kullanıcı beklemeden öneri görüyor.
3. Context Window: Uzun CV'ler için geniş context gerekiyor, Gemini yeterli.
Implementasyon:
javascript
// app/api/ai-suggest/route.ts (Edge Function)
import { GoogleGenerativeAI } from '@google/generative-ai'
export const runtime = 'edge' // Vercel Edge'de çalışıyor
export async function POST(req: Request) {
const { prompt, context } = await req.json()
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!)
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' })
const result = await model.generateContent([
`You are a CV writing expert. Based on this context: ${context}`,
`User input: ${prompt}`,
`Provide 2-3 improved versions. Be concise and specific.`
])
return Response.json({
suggestions: result.response.text()
})
}Gizlilik Notu: AI'ya gönderdiğimiz veri sadece o anlık prompt. Kullanıcının tam adı, telefonu gitmiyor. Sadece "Sales Manager, 5 years experience, increased revenue by 25%" gibi context.
PDF Generation: React-PDF + Puppeteer
CV'yi PDF'e çevirmek en zorlayıcı kısımlardan biriydi.
İlk Deneme: jsPDF
javascript
import jsPDF from 'jspdf'
const doc = new jsPDF()
doc.text('Name: John Doe', 10, 10)
doc.save('cv.pdf')Sorun: Layout kontrolü zor, karmaşık tasarımlar yapamıyorsunuz. ATS-uyumlu çıktı vermedi.
İkinci Deneme: html2canvas + jsPDF
HTML'i canvas'a çevir, canvas'ı PDF'e çevir.
Sorun: Font rendering berbat, metin seçilemiyordu (ATS okuyamaz).
Son Çözüm: Puppeteer (Headless Chrome)
javascript
// app/api/generate-pdf/route.ts
import puppeteer from 'puppeteer-core'
import chrome from 'chrome-aws-lambda'
export async function POST(req: Request) {
const { html, css } = await req.json()
const browser = await puppeteer.launch({
args: chrome.args,
executablePath: await chrome.executablePath,
})
const page = await browser.newPage()
await page.setContent(`
<html>
<head><style>${css}</style></head>
<body>${html}</body>
</html>
`)
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '0.5cm', bottom: '0.5cm' }
})
await browser.close()
return new Response(pdf, {
headers: { 'Content-Type': 'application/pdf' }
})
}Avantajlar:
Pixel-perfect rendering
Metin seçilebilir (ATS-uyumlu)
Tüm CSS özellikleri destekleniyor
Dezavantaj: Serverless ortamda Puppeteer koşturmak zor. chrome-aws-lambda paketi ile çözdük ama cold start süresi uzun (3-5 saniye).
Hemen ücretsiz ve güvenli CV'nizi oluşturun - PDF çıktımız hem görsel kalitesi yüksek hem ATS-uyumlu.
ATS Uyumluluk: Gerçek Zamanlı Kontrol
ATS sistemleri basit parsing yapıyor. Karmaşık HTML'i okuyamıyorlar. Bizim çözümümüz:
1. Semantic HTML
jsx
// ❌ Yanlış (ATS okuyamaz)
<div className="name">John Doe</div>
<div className="title">Software Engineer</div>
// ✅ Doğru
<h1>John Doe</h1>
<h2>Software Engineer</h2>2. Tek Sütun Düzen
css
/* Çift sütun kullanmıyoruz */
.cv-container {
display: flex;
flex-direction: column; /* Her zaman column */
max-width: 210mm; /* A4 genişliği */
}3. Gerçek Zamanlı ATS Skoru
javascript
function calculateATSScore(cvData) {
let score = 0
// Standart başlıklar var mı?
if (cvData.sections.includes('Work Experience')) score += 20
if (cvData.sections.includes('Education')) score += 20
// Tarih formatı tutarlı mı?
const dateFormats = cvData.dates.map(d => d.format)
if (new Set(dateFormats).size === 1) score += 15
// Grafik/tablo kullanmıyor mı?
if (!cvData.hasGraphics) score += 15
// Standart font mu?
if (['Arial', 'Calibri', 'Times New Roman'].includes(cvData.font)) {
score += 10
}
// Anahtar kelime yoğunluğu
const keywords = extractKeywords(cvData.targetJob)
const cvText = cvData.toString()
const matchRate = keywords.filter(k => cvText.includes(k)).length / keywords.length
score += matchRate * 20
return Math.min(score, 100)
}Kullanıcı yazarken, ATS skoru canlı güncelleniyor. %60'ın altındaysa uyarı gösteriyoruz.
Performans Optimizasyonları
1. Code Splitting
javascript
// Lazy loading ile sadece gerekli component'ler yükleniyor
const CVEditor = dynamic(() => import('@/components/CVEditor'), {
loading: () => <Skeleton />,
ssr: false // Client-side only
})
const PDFPreview = dynamic(() => import('@/components/PDFPreview'), {
loading: () => <Spinner />
})Sonuç: İlk sayfa yüklemesi 2.1MB → 890KB
2. Image Optimization
Next.js Image component otomatik optimize ediyor:
jsx
import Image from 'next/image'
<Image
src="/templates/modern.png"
width={400}
height={600}
alt="Modern CV Template"
loading="lazy"
quality={80} // 100 yerine 80, fark belli değil
/>Sonuç: Görsel boyutu %60 düştü, kalite aynı.
3. Debouncing for Autosave
Kullanıcı her tuşa bastığında localStorage'a yazarsak, performans düşer:
javascript
import { useDebouncedCallback } from 'use-debounce'
const debouncedSave = useDebouncedCallback(
(data) => {
localStorage.setItem('cv-draft', JSON.stringify(data))
},
500 // 500ms bekle
)
const handleChange = (e) => {
const newData = { ...cvData, [e.target.name]: e.target.value }
setCVData(newData)
debouncedSave(newData) // Debounced
}Sonuç: Input lag yok, smooth typing deneyimi.
4. Service Worker for Offline Support
javascript
// public/sw.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('cvhazirla-v1').then((cache) => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/main.js',
'/templates/modern.html'
])
})
)
})
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request)
})
)
})Sonuç: Offline'da bile temel özellikleri kullanılabiliyor.
Güvenlik ve Gizlilik Implementasyonu
1. XSS Koruması
javascript
import DOMPurify from 'isomorphic-dompurify'
const sanitizeInput = (input) => {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
})
}
// Kullanıcı input'u render etmeden önce sanitize et
<div dangerouslySetInnerHTML={{
__html: sanitizeInput(userInput)
}} />2. HTTPS Everywhere
javascript
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
}
]
}
]
}
}3. CSP (Content Security Policy)
javascript
// next.config.js
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self' https://generativelanguage.googleapis.com;
`
module.exports = {
async headers() {
return [
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
}
]
}
}Zorluklar ve Çözümler
Zorluk #1: Cross-Browser localStorage Davranışları
Problem: Safari Private Mode'da localStorage çalışmıyor. Hata fırlatıyor.
Çözüm:
javascript
function isLocalStorageAvailable() {
try {
const test = '__localStorage_test__'
localStorage.setItem(test, test)
localStorage.removeItem(test)
return true
} catch (e) {
return false
}
}
// Fallback: Memory storage
const storage = isLocalStorageAvailable()
? localStorage
: createMemoryStorage()Zorluk #2: PDF Generation Cold Start
Problem: Puppeteer cold start 5 saniye sürüyor, kullanıcı bekliyor.
Çözüm:
Loading state ile kullanıcıyı bilgilendirme
Warm-up fonksiyonu ile Lambda'yı sıcak tutma
Gelecek plan: PDF rendering'i client-side'a taşımak (jsPDF + canvas2pdf)
Zorluk #3: AI Halüsinasyonları
Problem: Gemini bazen alakasız veya yanlış öneriler üretiyor.
Çözüm:
javascript
const prompt = `
You are a CV writing assistant. Follow these rules strictly:
1. Only suggest improvements, never hallucinate experience
2. Use metrics and numbers when possible
3. Keep suggestions under 50 words
4. If you can't improve, say "Looks good as is"
Context: ${context}
User input: ${userInput}
`Prompt engineering ile halüsinasyonlar %80 azaldı.
Zorluk #4: Mobile Performance
Problem: Mobilde CV editörü yavaş, keyboard açılınca layout bozuluyor.
Çözüm:
Virtual keyboard için viewport ayarı
Lazy rendering (sadece görünen bölümler render)
Touch optimize edilmiş input'lar
javascript
// viewport fix
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />Deployment: Vercel
Neden Vercel?
Next.js'in yaratıcıları, mükemmel entegrasyon
Edge Functions için built-in support
Zero-config deployment (git push → production)
Global CDN, düşük latency
yaml
# vercel.json
{
"functions": {
"app/api/generate-pdf/route.ts": {
"memory": 3008,
"maxDuration": 30
}
},
"regions": ["iad1"] # EU için fra1
}Metrikler ve Sonuçlar
6 ay sonra neredeyiz?
Performance Metrics:
Lighthouse Score: 95/100
First Contentful Paint: 0.8s
Time to Interactive: 1.2s
Total Blocking Time: 100ms
User Metrics:
Ortalama CV hazırlama süresi: 22 dakika
Geri dönüş oranı: %67 (kullanıcılar tekrar geliyor)
Mobile kullanım: %43
Infrastructure:
Sunucu maliyeti: $12/ay (çünkü veri saklamıyoruz)
AI API maliyeti: Kullanıcı başına $0.002
Toplam maliyet: $50/ay (1000 kullanıcıya kadar)
Öğrenilenler
1. Local-First Gerçekten Çalışıyor
"Sunucuda veri saklamadan SaaS yapılmaz" diyenler yanılıyor. Çoğu use case için local-first yeterli ve daha iyi.
2. AI Oyunun Kurallarını Değiştiriyor
Eskiden "CV yazma" manuel işti. AI ile "AI-assisted CV writing" oluyor. Kullanıcı yine yazıyor ama yardım alıyor.
3. Privacy Artık Marketing Noktası
"Verilerinizi saklamıyoruz" demek önemli bir diferansiyatör. İnsanlar buna değer veriyor.
4. Performans Değil, Algılanan Performans
Kullanıcı 500ms bekliyor ama loading spinner güzel animasyonlu ise, "hızlı" diyor. UX > raw performance.
5. MVP Hızla Çıkar, Iterate Et
İlk versiyonda Puppeteer yoktu. Basit jsPDF vardı. Kullanıcılar şikayet etti, ekledik. Mükemmel olmadan başlayın.
Gelecek Planlar
1. Açık Kaynak
Kodun büyük kısmını açık kaynak yapma planındayız. Community'nin katkısıyla daha da iyi olacak.
2. Blockchain Doğrulama
CV'deki her bilginin blockchain'de doğrulanması. İşverenler "gerçekten bu şirkette çalıştı mı?" sorusunu cevaplayabilecek.
3. Video CV
AI ile video CV analizi ve öneri sistemi.
4. Multi-language Support
Şu an sadece Türkçe, İngilizce, Almanca, Fransızca gibi diller eklenecek.
Sonuç: Kod Şeffaflıkla Güven Kazanır
cvhazirla.app'i yazarken en önemli karar "transparency-first" yaklaşımıydı. Kullanıcıya "verileriniz güvende" demek yetmiyor. Nasıl güvende olduğunu teknik detaylarla anlatmak lazım.
Bu makale tam olarak bunu yapıyor. Kaynak kodları, mimari kararlar, zorluklar, çözümler... Hepsi açık.
Eğer siz de:
Privacy-first bir SaaS yapıyorsanız
Local-first mimariyi merak ediyorsanız
Next.js 14 ile modern bir app geliştiriyorsanız
AI entegrasyonu düşünüyorsanız
Umarım bu makale yardımcı olmuştur.
Hemen ücretsiz ve güvenli CV'nizi oluşturun - Ve arkasındaki teknolojiyi artık biliyorsunuz.
Sorularınız varsa: GitHub'da tartışma açabilir, katkıda bulunabilirsiniz. cvhazirla.app sadece bir product değil, bir açık kaynak yolculuğu.
Kod yazmak kolay. Güven kazanmak zor. Biz ikisini birden yapıyoruz.