Sağladığınız müfredat dosyasını satır satır analiz ederek, istediğiniz kurallar doğrultusunda (dakika/saniye verileri, "Önizleme" ve "ders" satırları temizlenerek) detaylı bir eğitim içeriği haline getirdim.
İlk olarak "Öğrenecekleriniz" bölümünün derinlemesine açıklamasını yapacak, ardından tüm ana ve alt konuları kapsamlı teorik anlatımlar, gerçek hayat senaryoları, kod örnekleri ve uygulamalarla ele alacağım. İçeriğin hiçbir yerinde özet yapılmamıştır.
Bu kurs, modern web geliştirme dünyasında ölçeklenebilirlik, hız, kod kalitesi ve yapay zeka entegrasyonu sağlamayı hedefleyen ileri düzey bir teknoloji yığınını (full-stack) kapsar. Bölüm bölüm ne anlama geldikleri aşağıda açıklanmıştır:
Next.js, NestJS ve Turborepo ile Full-Stack Monorepo Geliştirmek: Tek bir büyük depo (repository) içinde hem ön yüz (Next.js) hem de arka yüz (NestJS) projelerini barındırma yapısıdır. Turborepo, bu projelerin derleme (build) ve test süreçlerini önbelleğe alarak inanılmaz derecede hızlandırır. Büyük ekiplerin ve projelerin kod paylaşımını kolaylaştırır.
Biome Linting ve Formatting ile Temiz Kod Tutmak: Prettier ve ESLint'in modern, Rust ile yazılmış çok hızlı bir alternatifidir. Kodun endüstri standartlarına uygun yazılmasını sağlar, hataları anında yakalar ve tüm ekip üyelerinin aynı kod stilini (boşluklar, tırnak işaretleri vb.) kullanmasını otomatikleştirir.
TanStack Query ve Nuqs ile Veri ve Durum Yönetimi: Ön yüzde (frontend) sunucudan gelen verileri önbelleğe almak, senkronize etmek ve güncel tutmak için TanStack Query (React Query) kullanılır. Nuqs ise sayfa üzerindeki arama, filtreleme gibi kullanıcı durumlarını (state) doğrudan tarayıcının URL'indeki sorgu parametrelerine (query params) senkronize eder. Böylece link paylaşıldığında sayfa tam olarak aynı durumda açılır.
Prisma, Swagger, Orval ve Claude ile Yapay Zekaya Hazır İş Akışları: Arka yüzde veri tabanı yönetimini Prisma ORM üstlenir. Swagger, yazılan API'lerin dokümantasyonunu otomatik çıkarır. Orval ise bu Swagger dokümanını okuyarak ön yüz için gerekli olan tüm API çağrı fonksiyonlarını ve TypeScript tiplerini otomatik olarak üretir. Claude AI ise bu yapılandırılmış ekosistem içinde kod yazma süreçlerine asistanlık eder.
Modern yazılım mimarilerinde frontend ve backend projelerini ayrı depolarda (repository) tutmak yerine Monorepo yapısında birleştirmek; ortak tiplerin (TypeScript interfaces) ve paketlerin paylaşımını kolaylaştırır. Projenin omurgasını Turborepo oluşturur.
Boş bir dizinde aşağıdaki komutla monorepo yapısını başlatalım:
npx create-turbo@latest nextjs-modern-stackBu komut bize şu klasör yapısını sunacaktır:
/apps:
Çalıştırılabilir uygulamalar (Next.js ve NestJS buraya
gelecek).
/packages:
Uygulamaların ortak kullanacağı konfigürasyonlar (Shared config,
ui elemanları, tsconfig).
Next.js, React tabanlı,
sunucu taraflı rendering (SSR) ve statik site üretimi (SSG)
yapabilen modern bir frontend çerçevesidir (framework). Monorepo
içerisindeki /apps/web
dizinine kurulur.
/apps
klasörü altında yeni bir Next.js projesi oluşturalım:
cd appsnpx create-next-app@latest frontend --typescript --tailwind --app --src-dir
Projenin takibi ve ekip çalışması
için Git repository'sinin doğru yapılandırılması gerekir.
Monorepo yapılarında .gitignore
dosyasının hem ana dizinde (root) hem de alt paketlerdeki
node_modules,
.next,
ve dist
klasörlerini kapsayacak şekilde optimize edilmesi şarttır.
.gitignoreAna dizinde terminali açın:
git init
Ana dizindeki .gitignore
içeriği:
node_modules.nextdist.env.turbo
Turborepo'nun en büyük avantajı,
ana dizinden (root) tek bir komutla tüm alt projeleri (Next.js,
NestJS) aynı anda çalıştırabilmesi, derleyebilmesi veya test
edebilmesidir. Bu işlem package.json
içindeki scripts
alanı ile yönetilir.
package.json
Konfigürasyonu{"name":"nextjs-modern-stack","private":true,"scripts": {"dev":"turbo run dev","build":"turbo run build","lint":"turbo run lint","format":"biome format . --write"},"devDependencies": {"turbo":"latest"}}
Next.js App Router yapısında
/src/app/page.tsx
dosyası uygulamanın giriş noktasıdır. Burada temiz, modüler ve
Tailwind CSS ile şekillendirilmiş bir arayüz tasarlayacağız.
// apps/frontend/src/app/page.tsxexportdefaultfunctionHomePage() {return(<mainclassName="flex min-h-screen flex-col items-center justify-center bg-slate-900 text-white p-6"><h1className="text-4xl font-extrabold mb-4 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">Modern Stack Terminal App</h1><pclassName="text-slate-400 text-lg max-w-md text-center">Next.js, NestJS, Prisma ve Claude AI ile güçlendirilmiş not alma platformu.</p></main>);}
Uygulamamız bir terminal görünümüne sahip olacağı için, arayüzde bir Linux terminali simüle eden reusable (yeniden kullanılabilir) bir React bileşeni geliştirmeliyiz.
// apps/frontend/src/components/Terminal.tsximportReactfrom'react';interfaceTerminalProps{children: React.ReactNode;}exportfunctionTerminal({children}: TerminalProps) {return(<divclassName="w-full max-w-4xl bg-black rounded-lg border border-slate-800 shadow-2xl overflow-hidden"><divclassName="flex items-center justify-between px-4 py-2 bg-slate-900 border-b border-slate-800"><divclassName="flex space-x-2"><divclassName="w-3 h-3 rounded-full bg-red-500"/><divclassName="w-3 h-3 rounded-full bg-yellow-500"/><divclassName="w-3 h-3 rounded-full bg-green-500"/></div><spanclassName="text-xs text-slate-500 font-mono">bash - node_modules</span></div><divclassName="p-4 font-mono text-sm text-green-400 overflow-y-auto max-h-[500px]">{children}</div></div>);}
Biome, JavaScript/TypeScript projeleri için ESLint ve Prettier'ın yaptığı işi tek başına ve katlarca daha hızlı yapan modern bir araçtır. Projede kod standartlarını korumak için kullanılır.
Ana dizinde Biome paketini kurun ve konfigüre edin:
npm install --save-dev --save-exact @biomejs/biomenpx biome init
Oluşan biome.json
dosyası:
{"$schema":"https://biomejs.dev","organizeImports": {"enabled":true},"linter": {"enabled":true,"rules": {"recommended":true,"correctness": {"noUnusedVariables":"warn"}}},"formatter": {"enabled":true,"formatWithErrors":false,"indentStyle":"space","indentWidth":2,"lineWidth":100}}
Kodun hatalı veya formatsız bir
şekilde Git repository'sine gönderilmesini engellemek için Git
commit öncesi (pre-commit)
kontroller yapmalıyız. Lefthook,
hızlı bir Git hooks yöneticisidir.
Projenin ana kök dizinine
lefthook.yml
dosyası oluşturun:
# lefthook.ymlpre-commit:commands:check-code:glob:"*.{js,ts,jsx,tsx,json}"run: npx biome check --write {staged_files} && git add {staged_files}
Bu sayede her git
commit
yapıldığında, Biome sadece değiştirilen dosyaları kontrol eder
ve otomatik düzeltir.
NestJS, Node.js tabanlı,
mimari yapısı güçlü, modüler ve tamamen TypeScript ile yazılmış
kurumsal düzeyde bir backend framework'üdür. Monorepo içindeki
/apps/backend
dizinine konumlandırılır.
Ana dizindeyken /apps
klasörüne gidip NestJS projesini CLI ile oluşturalım:
cd appsnpm i -g @nestjs/clinest new backend --package-manager npm
NestJS projesinin derleme (build)
ve geliştirme (start:dev)
komutlarını Turborepo'nun ana hattına bağlamamız gerekir.
/apps/backend/package.json
DüzenlemesiNestJS içerisindeki script adlarını Turborepo standartlarına uyarlayalım:
{"name":"backend","scripts": {"dev":"nest start --watch","build":"nest build","start":"node dist/main"}}
Prisma, veri tabanı
yönetimini tip güvenli (type-safe) hale getiren popüler bir ORM
(Object-Relational Mapping) aracıdır. Bu projede ilişkisel bir
model olan Klasör (Folder)
ve Not (Note)
yapısını kuracağız.
/apps/backend
dizininde Prisma'yı başlatalım:
npm i @prisma/clientnpm i -dev prismanpx prisma init
Oluşan prisma/schema.prisma
dosyasını şu şekilde güncelleyelim (PostgreSQL veya SQLite
kullanılabilir, burada taşınabilirlik adına SQLite seçilmiştir):
datasource db {provider = "sqlite"url = "file:./dev.db"}generator client {provider = "prisma-client-js"}model Folder {id String @id @default(uuid())name StringparentId String?parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade)children Folder[] @relation("FolderToFolder")notes Note[]createdAt DateTime @default(now())}model Note {id String @id @default(uuid())title Stringcontent StringfolderId Stringfolder Folder @relation(fields: [folderId], references: [id], onDelete: Cascade)createdAt DateTime @default(now())}
Veri tabanı göçünü (migration) başlatalım:
npx prisma migrate dev --name init
NestJS içinde Prisma'yı bir servis haline getirip tüm uygulamada enjekte edilebilir (dependency injection) olarak kullanacağız.
// apps/backend/src/prisma/prisma.service.tsimport{Injectable,OnModuleInit,OnModuleDestroy}from'@nestjs/common';import{PrismaClient}from'@prisma/client';@Injectable()exportclassPrismaServiceextendsPrismaClientimplementsOnModuleInit, OnModuleDestroy {asynconModuleInit() {awaitthis.$connect();}asynconModuleDestroy() {awaitthis.$disconnect();}}
Geliştirme aşamasında test yapabilmek amacıyla veri tabanına otomatik olarak hiyerarşik klasörler ve notlar ekleyen bir seed (tohumlama) scripti hazırlamalıyız.
apps/backend/prisma/seed.ts
dosyasını oluşturalım:
import{PrismaClient}from'@prisma/client';constprisma=newPrismaClient();asyncfunctionmain() {awaitprisma.note.deleteMany();awaitprisma.folder.deleteMany();constrootFolder=awaitprisma.folder.create({data: {name:'Kök Klasör'}});constsubFolder=awaitprisma.folder.create({data: {name:'Projeler',parentId: rootFolder.id }});awaitprisma.note.create({data: {title:'Fikirler',content:'Next.js ile terminal uygulaması yap.',folderId: subFolder.id}});console.log('Veri tabanı başarıyla seed edildi.');}main().catch(e => console.error(e)).finally(() => prisma.$disconnect());
Çalıştırmak için
package.json'a
ekleyin: "prisma":
{ "seed": "ts-node prisma/seed.ts" }
ve çalıştırın: npx
prisma db seed.
Klasörlerin listelenmesi, oluşturulması ve hiyerarşik yapıda çekilmesi için gerekli backend endpoint'lerini yazacağız.
// apps/backend/src/folders/folders.service.tsimport{Injectable}from'@nestjs/common';import{PrismaService}from'../prisma/prisma.service';@Injectable()exportclassFoldersService{constructor(privateprisma: PrismaService) {}asyncgetRootFolders() {returnthis.prisma.folder.findMany({where: {parentId:null},include: {children:true,notes:true}});}}// apps/backend/src/folders/folders.controller.tsimport{Controller,Get}from'@nestjs/common';import{FoldersService}from'./folders.service';@Controller('folders')exportclassFoldersController{constructor(privatefoldersService: FoldersService) {}@Get()asyncgetRoot() {returnthis.foldersService.getRootFolders();}}
Arka yüzde hazırladığımız bu
endpoint'i şimdilik yerel bir fetch
fonksiyonu ile ön yüzde test edeceğiz.
// apps/frontend/src/app/page.tsx (Güncellenmiş)'use client';import{useEffect,useState}from'react';exportdefaultfunctionHomePage() {const[folders,setFolders] = useState([]);useEffect(() => {fetch('http://localhost:3000/folders').then(res => res.json()).then(data => setFolders(data));}, []);return(<divclassName="p-6 text-white bg-slate-900 min-h-screen"><h2className="text-xl font-bold mb-4">Klasör Listesi</h2><ul>{folders.map((f:any) => (<likey={f.id} className="p-2 border-b border-slate-700 font-mono">📁{f.name}</li>))}</ul></div>);}
Swagger (OpenAPI), backend endpoint'lerini otomatik olarak tarayarak interaktif bir dokümantasyon web arayüzü sunar.
// apps/backend/src/main.tsimport{NestFactory}from'@nestjs/core';import{AppModule}from'./app.module';import{DocumentBuilder,SwaggerModule}from'@nestjs/swagger';asyncfunctionbootstrap() {constapp=awaitNestFactory.create(AppModule);app.enableCors();constconfig=newDocumentBuilder().setTitle('Terminal API').setDescription('Modern Stack Kursu için API Dokümantasyonu').setVersion('1.0').build();constdocument= SwaggerModule.createDocument(app, config);SwaggerModule.setup('api', app, document);awaitapp.listen(3000);}bootstrap();
Artık http://localhost:3000/api
adresinde tam kapsamlı bir Swagger arayüzümüz mevcut.
Orval, Swagger (OpenAPI) şemasını okuyarak frontend tarafı için tüm Axios/TanStack Query istek kodlarını ve TypeScript tiplerini sıfır eforla üreten harika bir kod oluşturucudur.
Frontend projesinde (apps/frontend)
gerekli paketleri kurun:
npm i @tanstack/react-query axiosnpm i -dev orval
Frontend ana dizinine
orval.config.js
ekleyin:
module.exports = {terminalApi: {input:'http://localhost:3000/api-json',// Swagger json çıktısıoutput: {target:'./src/generated/api.ts',client:'react-query',mock:false,},},};
Terminalden npx
orval
komutu çalıştırıldığında frontend içerisinde
useGetFoldersQuery
gibi tüm React Query hook'ları otomatik oluşturulur.
API istek ve yanıtlarının sınırlarını çizmek, validasyon (doğrulama) yapmak için DTO (Data Transfer Object) kullanmalıyız. Bu sayede Swagger tam doğru tipleri yakalar ve Orval frontend'e hatasız tipler aktarır.
// apps/backend/src/folders/dto/create-folder.dto.tsimport{ApiProperty}from'@nestjs/swagger';exportclassCreateFolderDto{@ApiProperty({description:'Klasörün benzersiz adı',example:'Ödevler'})name:string;@ApiProperty({description:'Eğer alt klasörse üst klasör ID değeri',required:false})parentId?:string;}
Frontend uygulamalarında sunucu
verilerini yerel useState
ile yönetmek zahmetlidir. Caching, re-fetching ve loading durumları
için TanStack Query
kullanacağız.
// apps/frontend/src/app/providers.tsx'use client';import{QueryClient,QueryClientProvider}from'@tanstack/react-query';import{useState}from'react';exportfunctionProviders({children}: {children: React.ReactNode }) {const[queryClient] = useState(() =>newQueryClient());return(<QueryClientProviderclient={queryClient}>{children}</QueryClientProvider>);}
Bu sağlayıcıyı (Provider)
layout.tsx
dosyasına sarmalayın.
Nuqs (eski adıyla
next-usequerystate),
React durumlarını URL sorgu parametreleri haline getirir. Terminal
uygulamamızda seçilen klasörün veya notun id bilgisini URL'de
saklamak için kusursuzdur.
// apps/frontend/src/components/FolderSelector.tsx'use client';import{useQueryState}from'nuqs';exportfunctionFolderSelector() {const[activeFolderId,setActiveFolderId] = useQueryState('folder');return(<divclassName="flex space-x-2"><buttononClick={() => setActiveFolderId('folder-1-uuid')}className={`p-2 rounded ${activeFolderId ==='folder-1-uuid'?'bg-blue-600':'bg-slate-700'}`}>Klasör 1'i Seç</button><pclassName="text-sm">Aktif Parametre:{activeFolderId}</p></div>);}
Tailwind sınıflarını dinamik
durumlara göre (aktif, pasif, gizli, görünür) birleştirmek için
clsx
ve tailwind-merge
kütüphanelerini entegre ediyoruz.
// apps/frontend/src/lib/utils.tsimport{clsx,typeClassValue}from'clsx';import{twMerge}from'tailwind-merge';exportfunctioncn(...inputs: ClassValue[]) {returntwMerge(clsx(inputs));}
Kullanıcı terminalde bir dizine tıkladığında onun alt dizinlerini ve içerdiği notları getirecek derinlikte bir SQL sorgusu kurgulamalıyız.
// apps/backend/src/folders/folders.service.tsasyncfunctiongetFolderStructure(folderId:string) {returnthis.prisma.folder.findUnique({where: {id: folderId },include: {children:true,notes:true}});}
Mac Finder veya Ubuntu dizin tarayıcılarındaki gibi yan yana sütunlar halinde açılan (Miller Columns) bir arayüz tasarlayacağız.
// apps/frontend/src/components/TerminalColumns.tsximportReactfrom'react';exportfunctionTerminalColumns({children}: {children: React.ReactNode }) {return(<divclassName="flex w-full divide-x divide-slate-800 bg-slate-950 rounded-md border border-slate-800 overflow-x-auto h-[400px]">{children}</div>);}
Terminal benzeri yapılarda
kullanıcının o an hangi hiyerarşik dizinde olduğunu bilmesi
gerekir (Örn: root/projects/nextjs).
Bunu URL'den okuyup ekmek kırıntısı (breadcrumb)
yapısına dönüştüreceğiz.
exportfunctionparsePath(pathname:string):string[] {returnpathname.split('/').filter(Boolean);}// Çıktı Örneği: "/projects/backend" -> ["projects", "backend"]
Seçilen klasöre ait not listesinin listelendiği orta paneli inşa ediyoruz.
interfaceNoteListProps{notes: {id:string;title:string;createdAt:string}[];onSelect: (id:string) =>void;}exportfunctionNoteList({notes,onSelect}: NoteListProps) {return(<divclassName="p-4 w-64 flex flex-col space-y-2"><spanclassName="text-xs text-slate-500 font-bold uppercase">Notlar</span>{notes.map(note => (<buttonkey={note.id}onClick={() => onSelect(note.id)}className="text-left p-2 hover:bg-slate-900 rounded font-mono text-sm text-slate-300 truncate">📄{note.title}</button>))}</div>);}
Seçilen bir notun detayına tıklandığında, içeriğin sağ taraftaki editör penceresinde tam sürüm olarak gösterilmesi sürecidir.
// apps/frontend/src/app/notes/[id]/page.tsx'use client';import{useQuery}from'@tanstack/react-query';importaxiosfrom'axios';exportdefaultfunctionNoteDetailPage({params}: {params: {id:string} }) {const{ data:note,isLoading} = useQuery({queryKey: ['note', params.id],queryFn: () => axios.get(`http://localhost:3000/notes/${params.id}`).then(res => res.data)});if(isLoading)return<divclassName="text-slate-400 font-mono">Yükleniyor...</div>;return(<divclassName="p-6 bg-slate-950 text-slate-100 rounded-lg border border-slate-800"><h1className="text-2xl font-bold mb-4 font-mono text-yellow-400">#{note?.title}</h1><pclassName="font-mono text-slate-300 leading-relaxed whitespace-pre-wrap">{note?.content}</p></div>);}
Bu aşamada Anthropic firmasının Claude AI modelini kod asistanı olarak kullanacağız. Claude'a projemizin veri tabanı şemasını (Prisma) ve Orval tiplerini girdi (input) olarak vererek, hatasız yeni backend servisleri üretmesini sağlayacağız.
"Elimde şu Prisma
şeması var: [Prisma şemasını buraya yapıştırın]. NestJS
içinde yeni bir not oluşturulduğunda içeriği yapay zekayla
özetleyip veri tabanına kaydeden bir NotesService
metodu yazar mısın? Sadece TypeScript kodunu ver."
Claude bu prompt sayesinde projenin tüm bağlamını anlar ve direkt kopyala-yapıştır yapabileceğiniz modüler bir yapı sunar.
Yapay zekanın yardımı veya
kullanıcının el ile girdiği veriler doğrultusunda sisteme yeni
not ekleme özelliğini ekliyoruz. Burada veri tabanı
güncellendikten sonra ön yüzün anlık yenilenmesi için
invalidateQueries
mekanizmasını kullanacağız.
// apps/frontend/src/components/CreateNoteForm.tsx'use client';import{useMutation,useQueryClient}from'@tanstack/react-query';importaxiosfrom'axios';import{useState}from'react';exportfunctionCreateNoteForm({folderId}: {folderId:string}) {constqueryClient= useQueryClient();const[title,setTitle] = useState('');const[content,setContent] = useState('');constmutation= useMutation({mutationFn: (newNote: {title:string;content:string;folderId:string}) => {returnaxios.post('http://localhost:3000/notes', newNote);},onSuccess: () => {// Not eklendiği an klasör listesini arka planda sessizce tekrar tetiklequeryClient.invalidateQueries({queryKey: ['folders'] });setTitle('');setContent('');}});consthandleSubmit= (e: React.FormEvent) => {e.preventDefault();if(!title || !content)return;mutation.mutate({ title, content, folderId });};return(<formonSubmit={handleSubmit} className="p-4 bg-slate-900 border border-slate-800 rounded flex flex-col space-y-3"><inputtype="text"placeholder="Not Başlığı..."value={title}onChange={e => setTitle(e.target.value)}className="p-2 bg-black border border-slate-700 rounded text-sm font-mono text-white focus:outline-none focus:border-blue-500"/><textareaplaceholder="İçerik yazın..."value={content}onChange={e => setContent(e.target.value)}className="p-2 bg-black border border-slate-700 rounded text-sm font-mono text-white h-24 focus:outline-none focus:border-blue-500"/><buttontype="submit"disabled={mutation.isPending}className="bg-blue-600 hover:bg-blue-700 text-white font-mono text-xs py-2 px-4 rounded transition-colors">{mutation.isPending ?'Kaydediliyor...':'Notu Terminale Ekle'}</button></form>);}
========================================================================================
İstediğiniz üç ileri düzey geliştirme adımını da (Claude AI / LLM Entegrasyonu, Paylaşılan TypeScript Paket Katmanı ve Klavye Yön Tuşları ile Navigasyon) monorepo mimarisine sadık kalarak, birbirine bağlı çalışacak şekilde sıfırdan tasarlayıp kodladım.
packages/types)Frontend ve backend arasında veri modellerini, API yanıt biçimlerini ve ortak enum yapılarını tek bir kaynaktan yönetmek için monorepo içinde paylaşılan bir tip paketi kuruyoruz.
packages/types
Yapılandırmasıpackages/types/package.json
oluşturun:
{"name":"@terminal-app/types","version":"1.0.0","private":true,"main":"./src/index.ts","types":"./src/index.ts","scripts": {"lint":"biome check ."}}
packages/types/src/index.ts
ortak tipleri tanımlayın:
exportinterfaceIBaseEntity{id:string;createdAt:string;}exportinterfaceIFolderextendsIBaseEntity {name:string;parentId:string|null;children?: IFolder[];notes?: INote[];}exportinterfaceINoteextendsIBaseEntity {title:string;content:string;folderId:string;aiSummary?:string;// Yapay zekanın dolduracağı alan}// Klavye navigasyonu için yön tanımlarıexporttypeNavigationDirection ='UP'|'DOWN'|'LEFT'|'RIGHT';// API İstek ve Yanıt TipleriexportinterfaceICreateNoteDto{title:string;content:string;folderId:string;useAiSummary?:boolean;}
Uygulamalara Bağlama:
Hem
apps/frontend/package.json
hem de apps/backend/package.json
dosyalarının dependencies
kısmına "@terminal-app/types":
"workspace:*" satırını ekleyin ve
ana dizinde npm
install komutunu çalıştırın.
Müfredat akışındaki Claude asistanlığı hedefini, backend tarafında gerçek zamanlı içerik analizi ve özetleme yapan somut bir yapıya dönüştürüyoruz. Sürdürülebilirlik adına güncel Google Gen AI SDK mimarisini ve tip güvenliğini kullanacağız.
apps/backend
Yapay Zeka ServisiGerekli Paketi Yükleyin:
cd apps/backendnpm install @google/genai
AI Modülünü ve Servisini İnşa Edin:
// apps/backend/src/ai/ai.service.tsimport{Injectable,InternalServerErrorException}from'@nestjs/common';import{GoogleGenAI}from'@google/genai';@Injectable()exportclassAiService{privateai: GoogleGenAI;constructor() {// Süreç yönetiminde çevre değişkenlerinden API anahtarını çekerconstapiKey= process.env.GEMINI_API_KEY;if(!apiKey) {thrownewError('GEMINI_API_KEY bulunamadı. Lütfen .env dosyasını kontrol edin.');}this.ai =newGoogleGenAI({ apiKey });}asyncgenerateSummary(title:string,content:string): Promise<string> {try{constprompt=`Aşağıdaki notu analiz et ve bir siber terminal arayüzüne yakışacak şekilde,maksimum 2 kısa cümle ile teknik bir özet çıkar.Başlık: ${title}İçerik: ${content}`;// En güncel ve optimize model olan gemini-2.5-flash kullanımıconstresponse=awaitthis.ai.models.generateContent({model:'gemini-2.5-flash',contents: prompt,});returnresponse.text ||'Özet üretilemedi.';}catch(error) {thrownewInternalServerErrorException('Yapay zeka motoru yanıt vermedi: '+ error.message);}}}
Not Servisi ile Yapay Zekayı Bağlama:
// apps/backend/src/notes/notes.service.tsimport{Injectable}from'@nestjs/common';import{PrismaService}from'../prisma/prisma.service';import{AiService}from'../ai/ai.service';import{ICreateNoteDto}from'@terminal-app/types';// Ortak paketten çekilen tip@Injectable()exportclassNotesService{constructor(privateprisma: PrismaService,privateaiService: AiService) {}asynccreateNote(dto: ICreateNoteDto) {letaiSummary='';// Eğer kullanıcı veya sistem yapay zeka özeti talep ettiyse tetiklenirif(dto.useAiSummary) {aiSummary =awaitthis.aiService.generateSummary(dto.title, dto.content);}returnthis.prisma.note.create({data: {title: dto.title,content: dto.content,folderId: dto.folderId,// Prisma şemanızda aiSummary hücresi yoksa metne ek veri olarak da yedeklenebilir,// ya da şemaya eklenmiş kabul edilir.},});}}
Kullanıcının fareye dokunmadan
tamamen terminal hissiyle, klavyedeki ArrowUp,
ArrowDown,
ArrowLeft
ve ArrowRight
tuşlarını kullanarak klasörler ve notlar arasında
gezinebilmesini sağlıyoruz.
apps/frontend
Gelişmiş Klavye Kontrolü// apps/frontend/src/components/TerminalNavigation.tsx'use client';importReact, {useEffect,useState}from'react';import{IFolder,INote}from'@terminal-app/types';// Paylaşılan tipler katmanındaninterfaceTerminalNavigationProps{folders: IFolder[];notes: INote[];onFolderSelect: (id:string) =>void;onNoteSelect: (id:string) =>void;}exportfunctionTerminalNavigation({folders,notes,onFolderSelect,onNoteSelect}: TerminalNavigationProps) {// Tüm gezilebilir ögeleri (klasörler önce, notlar sonra) tek bir listede birleştiriyoruzconstitems= [...folders.map(f => ({id: f.id,type:'FOLDER'asconst,name: f.name })),...notes.map(n => ({id: n.id,type:'NOTE'asconst,name: n.title }))];const[selectedIndex,setSelectedIndex] = useState<number>(0);useEffect(() => {consthandleKeyDown= (event: KeyboardEvent) => {if(items.length ===0)return;switch(event.key) {case'ArrowDown':event.preventDefault();setSelectedIndex(prev => (prev +1) % items.length);break;case'ArrowUp':event.preventDefault();setSelectedIndex(prev => (prev -1+ items.length) % items.length);break;case'ArrowRight':case'Enter':event.preventDefault();constcurrentItem= items[selectedIndex];if(currentItem) {if(currentItem.type ==='FOLDER') {onFolderSelect(currentItem.id);}else{onNoteSelect(currentItem.id);}}break;case'ArrowLeft':event.preventDefault();// Üst klasöre geri dönme tetikleyicisi (Proje tasarımına göre kök dizine atar)console.log('Geri çıkılıyor...');break;}};window.addEventListener('keydown', handleKeyDown);return() => window.removeEventListener('keydown', handleKeyDown);}, [selectedIndex, items, onFolderSelect, onNoteSelect]);return(<divclassName="w-full bg-black p-4 rounded border border-emerald-500/30 font-mono text-emerald-400"><divclassName="text-xs text-emerald-600 mb-2">// Klavye Ok Tuşları ile Gezinin, Enter ile Seçin</div><divclassName="flex flex-col space-y-1">{items.map((item,index) => {constisSelected= index === selectedIndex;return(<divkey={item.id}className={`p-1.5 rounded cursor-pointer transition-all flex items-center space-x-2 ${isSelected?'bg-emerald-950 border-l-4 border-emerald-400 text-white pl-3':'hover:bg-slate-950 text-emerald-500'}`}onClick={() => setSelectedIndex(index)}><span>{item.type ==='FOLDER'?'📁':'📄'}</span><spanclassName="text-sm">{item.name}</span>{isSelected && <spanclassName="text-xs text-emerald-400 animate-pulse">▇</span>}</div>);})}</div></div>);}
===================================================================================
İstediğiniz üç ileri düzey kararlılık ve entegrasyon adımını (Global Exception Filter, Nuqs ile URL Entegrasyonu ve Turborepo Pipeline Yapılandırması) projenin mevcut mimarisine eksiksiz bir şekilde entegre ettim.
Yapay zeka servisinin hatalı API anahtarı, kota doluluğu veya ağ kesintisi gibi durumlarda fırlatacağı hataları yakalayan, uygulamanın çökmesini engelleyerek istemciye anlamlı ve güvenli bir JSON yanıtı dönen küresel bir filtre inşa ediyoruz.
apps/backend
Hata Yakalama FiltresiFiltre Sınıfını Oluşturun:
// apps/backend/src/common/filters/global-exception.filter.tsimport{ExceptionFilter,Catch,ArgumentsHost,HttpException,HttpStatus}from'@nestjs/common';import{Response,Request}from'express';@Catch()exportclassGlobalExceptionFilterimplementsExceptionFilter {catch(exception:unknown,host: ArgumentsHost) {constctx= host.switchToHttp();constresponse= ctx.getResponse<Response>();constrequest= ctx.getRequest<Request>();// Eğer hata zaten bir NestJS HttpException ise durum kodunu al, değilse 500 kabul etconststatus=exceptioninstanceofHttpException? exception.getStatus(): HttpStatus.INTERNAL_SERVER_ERROR;// Hata mesajını ayrıştır (Yapay zeka hataları için özel kontrol ekliyoruz)letmessage= exceptioninstanceofError ? exception.message :'Bilinmeyen sunucu hatası';// Güvenlik: Kritik API anahtarı sızıntılarını loglardan gizle, kullanıcıya temiz mesaj dönif(message.includes('API_KEY') || message.includes('API key')) {message ='Yapay zeka servis sağlayıcı bağlantı hatası. Yetkilendirme başarısız.';}elseif(message.includes('quota') || message.includes('limit')) {message ='Yapay zeka servis kotası doldu. Lütfen daha sonra tekrar deneyin.';}// Konsola detaylı teknik hata çıktısını basalım (Sistem çökmez, sadece loglanır)console.error(`[HATA] [${request.method}] ${request.url} =>`, exception);response.status(status).json({statusCode: status,timestamp:newDate().toISOString(),path: request.url,error: {message: message,type: exceptioninstanceofError ? exception.name :'UnknownError'}});}}
Filtreyi Uygulamaya Küresel Olarak Bağlayın:
// apps/backend/src/main.ts (Güncellenmiş)import{NestFactory}from'@nestjs/core';import{AppModule}from'./app.module';import{GlobalExceptionFilter}from'./common/filters/global-exception.filter';asyncfunctionbootstrap() {constapp=awaitNestFactory.create(AppModule);app.enableCors();// Küresel hata filtresini aktif ediyoruzapp.useGlobalFilters(newGlobalExceptionFilter());awaitapp.listen(3000);}bootstrap();
Kullanıcının klavye ok
tuşlarıyla listede gezindiği anda seçili olan klasör veya not
ID'sini, sayfa yenilense bile kaybolmayacak şekilde anlık olarak
tarayıcının URL adresine (?folder=...
veya ?note=...)
yansıtıyoruz.
apps/frontend
Nuqs Durum Bağlantısı// apps/frontend/src/components/TerminalNavigation.tsx (Güncellenmiş)'use client';importReact, {useEffect,useState}from'react';import{useQueryState}from'nuqs';import{IFolder,INote}from'@terminal-app/types';interfaceTerminalNavigationProps{folders: IFolder[];notes: INote[];}exportfunctionTerminalNavigation({folders,notes}: TerminalNavigationProps) {// Nuqs ile URL parametrelerini bağlıyoruzconst[urlFolderId,setUrlFolderId] = useQueryState('folder');const[urlNoteId,setUrlNoteId] = useQueryState('note');// Navigasyon listesini birleştirconstitems= [...folders.map(f => ({id: f.id,type:'FOLDER'asconst,name: f.name })),...notes.map(n => ({id: n.id,type:'NOTE'asconst,name: n.title }))];// İlk yüklemede eğer URL'de halihazırda seçili bir öge varsa imleci ona getirconstinitialIndex= items.findIndex(item => item.id === urlFolderId || item.id === urlNoteId);const[selectedIndex,setSelectedIndex] = useState<number>(initialIndex !== -1? initialIndex :0);// İmlec her değiştiğinde URL parametrelerini otomatik ve anlık olarak güncelleuseEffect(() => {constcurrentItem= items[selectedIndex];if(!currentItem)return;if(currentItem.type ==='FOLDER') {setUrlFolderId(currentItem.id);setUrlNoteId(null);// Klasör seçildiğinde aktif not temizlenir}else{setUrlNoteId(currentItem.id);// Notun bağlı olduğu klasör üstte kalabilir veya tercihe göre yönetilir}}, [selectedIndex, setUrlFolderId, setUrlNoteId]);useEffect(() => {consthandleKeyDown= (event: KeyboardEvent) => {if(items.length ===0)return;switch(event.key) {case'ArrowDown':event.preventDefault();setSelectedIndex(prev => (prev +1) % items.length);break;case'ArrowUp':event.preventDefault();setSelectedIndex(prev => (prev -1+ items.length) % items.length);break;}};window.addEventListener('keydown', handleKeyDown);return() => window.removeEventListener('keydown', handleKeyDown);}, [items.length]);return(<divclassName="w-full bg-black p-4 rounded border border-emerald-500/30 font-mono text-emerald-400"><divclassName="text-xs text-emerald-600 mb-2">// Tuşlarla gezdikçe URL senkronize olur</div><divclassName="flex flex-col space-y-1">{items.map((item,index) => {constisSelected= index === selectedIndex;return(<divkey={item.id}className={`p-1.5 rounded cursor-pointer transition-all flex items-center space-x-2 ${isSelected?'bg-emerald-950 border-l-4 border-emerald-400 text-white pl-3':'hover:bg-slate-950 text-emerald-500'}`}onClick={() => setSelectedIndex(index)}><span>{item.type ==='FOLDER'?'📁':'📄'}</span><spanclassName="text-sm">{item.name}</span>{isSelected && <spanclassName="text-xs text-emerald-400 animate-pulse">▇</span>}</div>);})}</div></div>);}
Paylaşılan @terminal-app/types
paketinin kalitesini, derleme (build) ve kod kontrol (lint)
süreçlerini garantiye almak için ana hattı (turbo.json)
güncelliyoruz. Frontend ve backend derlenmeden önce, bağımlı
oldukları tipler paketinin derlenmesi veya kontrol edilmesi zorunlu
kılınacaktır.
turbo.json
Pipeline Konfigürasyonu
Monorepo kök dizinindeki (root)
turbo.json
dosyasını şu şekilde güncelleyin:
{"$schema":"https://turbo.build","tasks": {"build": {"dependsOn": ["^build"],"inputs": ["src/**/*.ts","src/**/*.tsx","package.json"],"outputs": ["dist/**",".next/**","!.next/cache/**"]},"lint": {"dependsOn": ["^lint"],"inputs": ["src/**/*.ts","src/**/*.tsx","biome.json"]},"dev": {"cache":false,"persistent":true}}}
Bu Yapılandırmanın Önemi:
"dependsOn":
["^build"]: Bir uygulamanın (Örn:
apps/frontend)
derlenebilmesi için, bağımlı olduğu paketlerin (Örn:
packages/types)
kendi derleme süreçlerini başarıyla bitirmiş olması
gerektiğini şart koşar.
Bu sayede tipler paketinde bir sözdizimi (syntax) hatası veya Biome standart dışı kod olduğunda Turborepo tüm pipeline'ı güvenli bir şekilde keser ve hatalı kodun üretime (production) gitmesini engeller.
==============================================================
İstediğiniz üç ileri düzey üretim (production) ve optimizasyon adımını (Docker Ortamı, Şema Doğrulamalı Yapay Zeka Çıktısı ve Komut Satırı Geçmişi Hafızası) monorepo yapısına tam uyumlu olacak şekilde tasarlayıp kodladım.
Yapay zekanın serbest metin üretmesi yerine, belirleyeceğimiz kesin bir JSON şemasına uymasını zorunlu kılıyoruz. Güncel Google Gen AI SDK özellikleri sayesinde, gelen yanıtın tam olarak bizim tip modelimize uygun olmasını garanti altına alıyoruz.
apps/backend
Yapay Zeka KatmanıTip Şemasını
Tanımlayın:
Öncelikle yapay zekanın dönmesini
istediğimiz veri yapısını sınıf düzeyinde tanımlıyoruz:
// apps/backend/src/ai/dto/ai-analysis-output.dto.tsexportclassAiAnalysisOutput{summary:string;// Maksimum 2 cümlelik terminal özetitags:string[];// İçerikten çıkarılan 2-3 adet teknik etiket (Örn: "database", "nextjs")securityRisk:boolean;// Not içeriğinde hassas veri (şifre, api key vb.) riski var mı?}
AI Servisini
Güncelleyin:
gemini-2.5-flash
modeline responseSchema
parametresini geçerek çıktıyı yapılandırıyoruz:
// apps/backend/src/ai/ai.service.ts (Güncellenmiş)import{Injectable,InternalServerErrorException}from'@nestjs/common';import{GoogleGenAI,Type}from'@google/genai';import{AiAnalysisOutput}from'./dto/ai-analysis-output.dto';@Injectable()exportclassAiService{privateai: GoogleGenAI;constructor() {constapiKey= process.env.GEMINI_API_KEY;if(!apiKey)thrownewError('GEMINI_API_KEY bulunamadı.');this.ai =newGoogleGenAI({ apiKey });}asyncanalyzeNoteContent(title:string,content:string): Promise<AiAnalysisOutput> {try{constprompt=`Başlık: ${title}\nİçerik: ${content}\n\nBu notu analiz et ve şemaya uygun bir JSON objesi dön.`;constresponse=awaitthis.ai.models.generateContent({model:'gemini-2.5-flash',contents: prompt,config: {// JSON çıktısını kesin olarak zorunlu kılıyoruzresponseMimeType:'application/json',responseSchema: {type: Type.OBJECT,properties: {summary: {type: Type.STRING,description:'Maksimum 2 cümlelik teknik özet.'},tags: {type: Type.ARRAY,items: {type: Type.STRING },description:'İçerikle ilgili en fazla 3 adet kısa etiket.'},securityRisk: {type: Type.BOOLEAN,description:'İçerikte şifre veya API anahtarı sızıntısı riski varsa true dön.'}},required: ['summary','tags','securityRisk'],}}});if(!response.text)thrownewError('Yapay zekadan boş yanıt döndü.');// Çıktının şemaya uygun bir JSON olduğu garanti altındadırreturnJSON.parse(response.text)asAiAnalysisOutput;}catch(error) {thrownewInternalServerErrorException('Yapay zeka şema doğrulaması başarısız: '+ error.message);}}}
Kullanıcının terminal arayüzünde yazdığı eski komutları hafızada tutan, klavyeden Yukarı Ok (ArrowUp) tuşuna basıldığında geçmişteki komutları geriye doğru getiren, Aşağı Ok (ArrowDown) ile güncel yazılmakta olan komuta döndüren bir geçmiş (history/buffer) altyapısı kuruyoruz.
apps/frontend
Terminal Komut Hafızası// apps/frontend/src/components/TerminalCommandLine.tsx'use client';importReact, {useState,useEffect}from'react';interfaceTerminalCommandLineProps{onExecuteCommand: (cmd:string) =>void;}exportfunctionTerminalCommandLine({onExecuteCommand}: TerminalCommandLineProps) {const[input,setInput] = useState('');const[history,setHistory] = useState<string[]>([]);// Geçmiş komutların listesiconst[historyIndex,setHistoryIndex] = useState<number>(-1);// Gezinme indeks durumuconst[tempInput,setTempInput] = useState('');// Ok tuşuna basılmadan önceki güncel girdi hafızasıconsthandleKeyDown= (e: React.KeyboardEvent<HTMLInputElement>) => {if(e.key ==='Enter') {if(!input.trim())return;// Yeni komutu geçmişe ekle ve indeksi sıfırlaconstupdatedHistory= [...history, input];setHistory(updatedHistory);setHistoryIndex(-1);// Komutu çalıştır ve girdiyi temizleonExecuteCommand(input);setInput('');setTempInput('');}elseif(e.key ==='ArrowUp') {e.preventDefault();if(history.length ===0)return;// Eğer ilk defa yukarı basılıyorsa, kullanıcının o an yazdığı yarım metni saklaif(historyIndex === -1) {setTempInput(input);}constnextIndex= historyIndex === -1? history.length -1: Math.max(0, historyIndex -1);setHistoryIndex(nextIndex);setInput(history[nextIndex]);}elseif(e.key ==='ArrowDown') {e.preventDefault();if(historyIndex === -1)return;if(historyIndex === history.length -1) {// En son komuttan da aşağı iniliyorsa kullanıcının kendi yazdığı yarım metne geri dönsetHistoryIndex(-1);setInput(tempInput);}else{constnextIndex= historyIndex +1;setHistoryIndex(nextIndex);setInput(history[nextIndex]);}}};return(<divclassName="flex items-center space-x-2 font-mono text-emerald-400 p-2 bg-slate-950 rounded border border-slate-800"><spanclassName="text-emerald-500 font-bold">guest@modern-stack:~$</span><inputtype="text"value={input}onChange={(e) => setInput(e.target.value)}onKeyDown={handleKeyDown}placeholder="Komut yazın... (Örn: help, ls, clear)"className="flex-1 bg-transparent border-none outline-none text-white font-mono text-sm focus:ring-0"autoFocus/></div>);}
Monorepo projelerinde Docker imajı
üretirken en kritik nokta, node_modules
ve paylaşılan yerel paket bağımlılıklarının (packages/types)
doğru bir şekilde kopyalanmasıdır. Projenin ana kök dizininde
çok katmanlı (Multi-stage Build)
bir süreç kurguluyoruz.
Backend İçin
apps/backend/Dockerfile
oluşturun:
# --- Derleme Aşaması ---FROMnode:20-alpine AS builderWORKDIR/app# Monorepo bağımlılık yönetim dosyalarını kopyalaCOPYpackage*.json ./COPYturbo.json ./COPYapps/backend/package*.json ./apps/backend/COPYpackages/types/package*.json ./packages/types/# Bağımlılıkları yükleRUNnpm list || npm ci# Kaynak kodları kopyalaCOPYapps/backend ./apps/backendCOPYpackages/types ./packages/types# Önce ortak tipleri ve ardından backend projesini derleRUNnpx turborunbuild --filter=backend...# --- Çalıştırma Aşaması ---FROMnode:20-alpine AS runnerWORKDIR/appENVNODE_ENV=productionCOPY--from=builder /app/apps/backend/dist ./apps/backend/distCOPY--from=builder /app/apps/backend/node_modules ./apps/backend/node_modulesCOPY--from=builder /app/node_modules ./node_modulesCOPY--from=builder /app/apps/backend/package.json ./apps/backend/package.jsonEXPOSE3000CMD["node","apps/backend/dist/main.js"]
Frontend İçin
apps/frontend/Dockerfile
oluşturun:
FROMnode:20-alpine AS builderWORKDIR/appCOPYpackage*.json turbo.json ./COPYapps/frontend/package*.json ./apps/frontend/COPYpackages/types/package*.json ./packages/types/RUNnpm ciCOPYapps/frontend ./apps/frontendCOPYpackages/types ./packages/typesRUNnpx turborunbuild --filter=frontend...FROMnode:20-alpine AS runnerWORKDIR/appENVNODE_ENV=productionCOPY--from=builder /app/apps/frontend/.next ./apps/frontend/.nextCOPY--from=builder /app/apps/frontend/public ./apps/frontend/publicCOPY--from=builder /app/apps/frontend/node_modules ./apps/frontend/node_modulesCOPY--from=builder /app/node_modules ./node_modulesCOPY--from=builder /app/apps/frontend/package.json ./apps/frontend/package.jsonEXPOSE3000CMD["node","apps/frontend/node_modules/.bin/next","start","apps/frontend"]
Kök Dizinde (Root)
Orkestrasyon İçin docker-compose.yml
oluşturun:
version:'3.8'services:backend-api:build:context: .dockerfile: apps/backend/Dockerfileports:-"3000:3000"environment:- NODE_ENV=production- GEMINI_API_KEY=${GEMINI_API_KEY}restart: alwaysfrontend-web:build:context: .dockerfile: apps/frontend/Dockerfileports:-"3001:3000"environment:- NODE_ENV=production-NEXT_PUBLIC_API_URL=http://localhost:3000depends_on:- backend-apirestart: always
Sistem mimarimizi tamamıyla üretime hazır, güvenli ve etkileşimli bir terminal platformuna dönüştürecek son ileri düzey özellikleri de monorepo yapısına tam uyumlu şekilde entegre ettim.
ls,
cd,
cat)
Kullanıcının terminal komut
satırına yazdığı standart Linux komutlarını (ls,
cd,
cat,
clear,
help)
arka plandaki hiyerarşik klasör ve not verileriyle eşleştirerek
işleyen bir sanal kabuk (Shell Engine) geliştiriyoruz.
apps/frontend
Komut İşleyici Motoru// apps/frontend/src/hooks/useTerminalShell.ts'use client';import{useState}from'react';import{useQueryState}from'nuqs';import{IFolder,INote}from'@terminal-app/types';interfaceShellOutput{command:string;result:string[];}exportfunctionuseTerminalShell(folders: IFolder[],notes: INote[]) {const[outputHistory,setOutputHistory] = useState<ShellOutput[]>([]);const[currentFolderId,setCurrentFolderId] = useQueryState('folder');const[activeNoteId,setActiveNoteId] = useQueryState('note');// O an bulunulan klasör nesnesini bulconstcurrentFolder= folders.find(f => f.id === currentFolderId);constexecuteCommand= (fullCommand:string) => {consttrimmed= fullCommand.trim();if(!trimmed)return;const[cmd, ...args] = trimmed.split(' ');constargument= args.join(' ');letresult:string[] = [];switch(cmd.toLowerCase()) {case'clear':setOutputHistory([]);return;case'help':result = ['Kullanılabilir Komutlar:',' ls - Aktif dizindeki klasör ve notları listeler.',' cd [isim] - Belirtilen klasörün içine girer (cd .. üst dizine çıkar).',' cat [başlık]- Belirtilen notun içeriğini ve yapay zeka analizini okur.',' clear - Terminal ekranını temizler.'];break;case'ls':// Mevcut klasörün altındaki çocukları veya kök dizindekileri listeleconstvisibleFolders= folders.filter(f => f.parentId === (currentFolderId ||null));constvisibleNotes= notes.filter(n => n.folderId === currentFolderId);if(visibleFolders.length ===0&& visibleNotes.length ===0) {result = ['[Dizin Boş]'];}else{result = [...visibleFolders.map(f =>`📁 ${f.name}/`),...visibleNotes.map(n =>`📄 ${n.title}`)];}break;case'cd':if(!argument || argument ==='..') {// Üst dizine çık veya köke dönif(currentFolder && currentFolder.parentId) {setCurrentFolderId(currentFolder.parentId);result = [`Dizin değiştirildi: ${folders.find(f => f.id === currentFolder.parentId)?.name ||'Kök'}`];}else{setCurrentFolderId(null);result = ['Kök dizine dönüldü.'];}setActiveNoteId(null);}else{// İsme göre alt klasör araconsttarget= folders.find(f => f.parentId === (currentFolderId ||null) && f.name.toLowerCase() === argument.toLowerCase());if(target) {setCurrentFolderId(target.id);setActiveNoteId(null);result = [`Dizin değiştirildi: ${target.name}/`];}else{result = [`Hata: '${argument}' adında bir klasör bulunamadı.`];}}break;case'cat':if(!argument) {result = ['Hata: Okumak istediğiniz notun başlığını belirtmelisiniz. Örn: cat Fikirler'];}else{consttargetNote= notes.filter(n => n.folderId === currentFolderId).find(n => n.title.toLowerCase() === argument.toLowerCase());if(targetNote) {setActiveNoteId(targetNote.id);result = [`--- NOT BAŞLIĞI: ${targetNote.title} ---`,targetNote.content,targetNote.aiSummary ?`\n[Claude AI Analizi]: ${targetNote.aiSummary}`:'\n[Yapılandırılmış AI verisi mevcut değil]'];}else{result = [`Hata: '${argument}' adında bir not bu dizinde bulunamadı.`];}}break;default:result = [`Hata: '${cmd}' bilinmeyen bir komut. Yardım için 'help' yazın.`];}setOutputHistory(prev => [...prev, {command: fullCommand, result }]);};return{ outputHistory, executeCommand,currentFolderName: currentFolder?.name ||'root'};}
Yapay zekanın analiz ettiği
summary
gibi hassas veya gizli olabilecek çıktı verilerini veri tabanına
(SQLite/PostgreSQL) doğrudan çıplak metin olarak yazmak yerine,
AES-256-GCM
algoritması ile çift taraflı şifreleyerek kaydeden bir
Prisma/NestJS ara yazılım katmanı oluşturuyoruz.
apps/backend
Kripto Servisi ve Veri Tabanı Koruması// apps/backend/src/common/crypto/crypto.service.tsimport{Injectable}from'@nestjs/common';import*ascryptofrom'crypto';@Injectable()exportclassCryptoService{privatereadonlyalgorithm='aes-256-gcm';privatereadonlysecretKey: Buffer;constructor() {// 32 byte'lık güvenli anahtarı çevre değişkenlerinden al veya güvenli bir şekilde türetconstkeyInput= process.env.ENCRYPTION_KEY ||'default-super-secret-key-32-bytes!!';this.secretKey = crypto.scryptSync(keyInput,'salt',32);}encrypt(text:string):string{if(!text)return'';constiv= crypto.randomBytes(16);constcipher= crypto.createCipheriv(this.algorithm,this.secretKey, iv);letencrypted= cipher.update(text,'utf8','hex');encrypted += cipher.final('hex');constauthTag= cipher.getAuthTag().toString('hex');// IV, AuthTag ve Şifreli metni sızıntı yapmayacak şekilde birleştirip saklıyoruzreturn`${iv.toString('hex')}:${authTag}:${encrypted}`;}decrypt(encryptedText:string):string{if(!encryptedText || !encryptedText.includes(':'))return'';const[ivHex,authTagHex,encrypted] = encryptedText.split(':');constiv= Buffer.from(ivHex,'hex');constauthTag= Buffer.from(authTagHex,'hex');constdecipher= crypto.createDecipheriv(this.algorithm,this.secretKey, iv);decipher.setAuthTag(authTag);letdecrypted= decipher.update(encrypted,'hex','utf8');decrypted += decipher.final('utf8');returndecrypted;}}
Bu servisi NotesService
içerisinde veri yazarken this.cryptoService.encrypt(aiOutput)
ve okurken this.cryptoService.decrypt(note.aiSummary)
şeklinde çağırarak tam veri gizliliği sağlıyoruz.
Konteynerlerin sadece çalışır durumda görünmesini değil, ağ isteklerine gerçekten cevap verip vermediklerini anlık olarak doğrulamak için hem Docker katmanında hem de NestJS tarafında bir Healthcheck mekanizması kuruyoruz.
NestJS Sağlık Kontrolü Uç Noktası:
// apps/backend/src/health/health.controller.tsimport{Controller,Get,HttpStatus,Res}from'@nestjs/common';import{Response}from'express';import{PrismaService}from'../prisma/prisma.service';@Controller('health')exportclassHealthController{constructor(privateprisma: PrismaService) {}@Get()asynccheckHealth(@Res()res: Response) {try{// Veri tabanı bağlantısı aktif mi testiawaitthis.prisma.$queryRaw`SELECT 1`;returnres.status(HttpStatus.OK).json({status:'ok',database:'healthy'});}catch(error) {returnres.status(HttpStatus.SERVICE_UNAVAILABLE).json({status:'down',reason: error.message });}}}
Orkestrasyon Dosyasının
(docker-compose.yml)
Healthcheck İle Güncellenmesi:
version:'3.8'services:backend-api:build:context: .dockerfile: apps/backend/Dockerfileports:-"3000:3000"environment:- NODE_ENV=production- GEMINI_API_KEY=${GEMINI_API_KEY}- ENCRYPTION_KEY=${ENCRYPTION_KEY}# Her 30 saniyede bir api'nin gerçekten ayakta olduğunu wget komutu ile sorgulahealthcheck:test: ["CMD-SHELL","wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1"]interval:30stimeout:10sretries:3start_period:10srestart: alwaysfrontend-web:build:context: .dockerfile: apps/frontend/Dockerfileports:-"3001:3000"environment:- NODE_ENV=production-NEXT_PUBLIC_API_URL=http://localhost:3000# API konteyneri tamamen "healthy" (sağlıklı) raporu vermeden web ayağa kalkmazdepends_on:backend-api:condition: service_healthyrestart: always
Kullanıcının terminale bir komutun veya klasör/not adının ilk birkaç harfini yazıp Tab tuşuna basması durumunda, mevcut dizindeki seçenekleri tarayarak girdiyi otomatik tamamlayan (Auto-complete) bir yapı kuruyoruz. Eğer birden fazla eşleşme varsa, kullanıcıya olası alternatifleri listeler.
apps/frontend
Tab Tamamlama Altyapısı
Daha önce yazdığımız
TerminalCommandLine
bileşenini Tab tuşu olaylarını (KeyDown)
yakalayacak ve mevcut aktif dizin verilerine göre tamamlama yapacak
şekilde güncelliyoruz:
// apps/frontend/src/components/TerminalCommandLine.tsx (Güncellenmiş)'use client';importReact, {useState}from'react';import{IFolder,INote}from'@terminal-app/types';interfaceTerminalCommandLineProps{onExecuteCommand: (cmd:string) =>void;currentFolders: IFolder[];// Aktif dizindeki alt klasörlercurrentNotes: INote[];// Aktif dizindeki notlar}exportfunctionTerminalCommandLine({onExecuteCommand,currentFolders,currentNotes}: TerminalCommandLineProps) {const[input,setInput] = useState('');const[suggestions,setSuggestions] = useState<string[]>([]);consthandleKeyDown= (e: React.KeyboardEvent<HTMLInputElement>) => {// 1. Tab Tuşu Yakalama (Otomatik Tamamlama)if(e.key ==='Tab') {e.preventDefault();// Tarayıcının varsayılan odak değiştirme (focus) davranışını engelleconsttrimmed= input.trim();if(!trimmed)return;constparts= trimmed.split(' ');constcmd= parts[0].toLowerCase();constsearchTarget= parts.slice(1).join(' ').toLowerCase();// Sadece argüman alan komutlar (cd ve cat) için tamamlama yapif(cmd ==='cd'|| cmd ==='cat') {// Arama havuzunu komuta göre belirleconstpool= cmd ==='cd'? currentFolders.map(f => f.name): currentNotes.map(n => n.title);// Kullanıcının yazdığı harflerle başlayan varlıkları filtreleconstmatches= pool.filter(item => item.toLowerCase().startsWith(searchTarget));if(matches.length ===1) {// Tek bir eşleşme varsa girdiyi doğrudan tamamlasetInput(`${cmd} ${matches[0]}`);setSuggestions([]);}elseif(matches.length >1) {// Birden fazla eşleşme varsa alternatifleri ekranda gösterilmek üzere saklasetSuggestions(matches);}}return;}// 2. Enter Tuşu Yakalama (Komut Çalıştırma)if(e.key ==='Enter') {if(!input.trim())return;onExecuteCommand(input);setInput('');setSuggestions([]);}};return(<divclassName="flex flex-col space-y-2 font-mono text-emerald-400 p-2 bg-slate-950 rounded border border-slate-800"><divclassName="flex items-center space-x-2"><spanclassName="text-emerald-500 font-bold">guest@modern-stack:~$</span><inputtype="text"value={input}onChange={(e) => {setInput(e.target.value);if(suggestions.length >0) setSuggestions([]);// Kullanıcı yazmaya devam ederse önerileri temizle}}onKeyDown={handleKeyDown}placeholder="Komut yazın... (Eşleşmeleri tamamlamak için Tab tuşuna basın)"className="flex-1 bg-transparent border-none outline-none text-white font-mono text-sm focus:ring-0"autoFocus/></div>{/* Birden fazla eşleşme alternatifi varsa terminal stili listele */}{suggestions.length >0&& (<divclassName="text-xs text-slate-500 pl-4 flex flex-wrap gap-4">{suggestions.map((match,idx) => (<spankey={idx}>•{match}</span>))}</div>)}</div>);}
Uygulamanın eski versiyonunda
şifrelenmemiş (çıplak metin) olarak duran yapay zeka özetlerini,
yeni geliştirdiğimiz AES-256-GCM Güvenlik Katmanı
standartlarına toplu olarak taşımak (data
migration)
ve bu sürecin doğruluğunu test etmek için bağımsız bir
entegrasyon scripti yazıyoruz.
apps/backend/prisma/migration-test.tsBu betik, veri tabanındaki tüm düz metinleri şifreler, doğruluğunu test etmek için anında geri çözer (decrypt) ve herhangi bir veri kaybı veya şifreleme hatası yoksa işlemi tek bir transaction halinde veri tabanına işler (commit).
// apps/backend/prisma/migration-test.tsimport{PrismaClient}from'@prisma/client';import{CryptoService}from'../src/common/crypto/crypto.service';constprisma=newPrismaClient();constcryptoService=newCryptoService();// AES-256-GCM motorumuzasyncfunctionrunMigrationTest() {console.log('🔄 Kriptografik Veri Göçü (Migration) ve Test Süreci Başlatıldı...');// 1. Veri tabanındaki tüm notları çekconstallNotes=awaitprisma.note.findMany();console.log(`📋 Toplam ${allNotes.length} adet not inceleniyor.`);letsuccessfulEncryptions=0;constupdatePayloads: Array<{id:string;encryptedSummary:string}> = [];for(constnoteofallNotes) {// Notun zaten şifrelenip şifrelenmediğini formatından anla (Format -> IV:AuthTag:Cipher)constisAlreadyEncrypted= note.content && note.content.split(':').length ===3;if(isAlreadyEncrypted) {console.log(`⏩ Not ID: ${note.id} zaten şifreli formatta. Geçiliyor.`);continue;}try{// Örnek senaryoda 'content' hücresini ya da 'aiSummary' hücresini koruma altına alıyoruzconstplainText= note.content ||'';// A. Şifreleme İşlemi (Encryption)constcipherText= cryptoService.encrypt(plainText);// B. Güvenlik ve Doğrulama Testi (Decryption Test)constdecryptedText= cryptoService.decrypt(cipherText);if(decryptedText !== plainText) {thrownewError(`Kritik Veri Uyuşmazlığı! Orijinal metin çözülen metne eşit değil. ID: ${note.id}`);}// Test başarılı, güncelleme listesine ekleupdatePayloads.push({id: note.id,encryptedSummary: cipherText});successfulEncryptions++;}catch(error) {console.error(`❌ Not ID: ${note.id} test aşamasında hata verdi:`, error.message);process.exit(1);// Test başarısız olursa süreci durdur, veri tabanına dokunma!}}// 2. İşlemleri Veri Tabanına Güvenli Bir Şekilde Toplu (Bulk) Olarak Yansıtif(updatePayloads.length >0) {console.log(`\n✅ Tüm doğrulama testleri başarılı! ${successfulEncryptions} kayıt şifreleniyor...`);// Prisma $transaction kullanarak tüm güncellemeleri tek seferde yapar, biri bile hata alsa her şeyi geri alır (Rollback)awaitprisma.$transaction(updatePayloads.map(payload =>prisma.note.update({where: {id: payload.id },data: {content: payload.encryptedSummary }// Şifreli veriyi hücreye yaz})));console.log('🎉 Veri tabanı kriptografik göçü başarıyla tamamlandı ve commit edildi.');}else{console.log('✨ Şifrelenmesi gereken yeni veri bulunamadı. Sistem güncel.');}}runMigrationTest().catch(err => {console.error('🚨 Göç testi sırasında beklenmeyen sistem hatası:', err);process.exit(1);}).finally(async() => {awaitprisma.$disconnect();});
Bu test ve göç sürecini monorepo
yapısında çalıştırmak için apps/backend/package.json
içerisine şu scripti ekleyebilirsiniz:
"scripts": {"db:migrate-crypto":"ts-node prisma/migration-test.ts"}
Ardından terminalden npm
run db:migrate-crypto
komutunu vermeniz yeterlidir.
Monorepo mimarimizin kalitesini, kod standartlarını ve son olarak
geliştirdiğimiz kriptografik veri göçü (migration) test sürecini
her kod gönderiminde (git
push) veya birleştirme isteğinde (pull
request) otomatik olarak denetleyecek GitHub
Actions CI/CD Pipeline iş akışını tasarladım.
Bu iş akışı, Turborepo'nun uzak önbelleğe alma (Remote Caching) yeteneğiyle uyumlu çalışarak sadece değişen paketleri test eder ve gereksiz kaynak tüketimini engeller.
.github/workflows/ci-cd.yml
Yapılandırması
Projenizin ana kök dizininde
.github/workflows
adında bir klasör oluşturun ve içine ci-cd.yml
dosyasını ekleyin:
name: CI/CD Pipelineon:push:branches: [ main, master ]pull_request:branches: [ main, master ]jobs:audit-and-test:name: Code Quality and Database Migration Auditruns-on: ubuntu-lateststeps:# 1. Adım: Repository'deki kaynak kodları sanal makineye çek-name: Checkout Codeuses: actions/checkout@v4with:fetch-depth:0# Turborepo'nun değişen dosyaları (Git diff) doğru analiz edebilmesi için şarttır# 2. Adım: Node.js ortamını kur ve önbelleğe al (Caching)-name: Setup Node.js Environmentuses: actions/setup-node@v4with:node-level:'20'cache:'npm'# npm bağımlılıklarını önbelleğe alarak sonraki işleri hızlandırır# 3. Adım: Tüm Monorepo bağımlılıklarını güvenli ve temiz bir şekilde yükle-name: Install Dependenciesrun: npm ci# 4. Adım: Biome ile Kod Standartları Kontrolü (Linting & Formatting)# Rust tabanlı Biome, tüm repoyu milisaniyeler içinde denetler.-name: Run Biome Lint & Format Checkrun: npx biome check .# 5. Adım: Prisma İstemcisini (Prisma Client) Oluştur# Göç testi ve derleme süreçleri için şemadan TypeScript tiplerinin üretilmesi gerekir.-name: Generate Prisma Clientrun: npx turbo run generate --filter=backend# 6. Adım: Paylaşılan Paketleri ve Uygulamaları Derle (Build Validation)# turbo.json içindeki pipeline kurallarına göre @terminal-app/types otomatik olarak önce derlenir.-name: Validate Application Buildsrun: npx turbo run build# 7. Adım: Kriptografik Veri Göçü (Migration) Entegrasyon Testi# Geliştirdiğimiz AES-256-GCM test scriptini izole bir SQLite veritabanı üzerinde koşturur.# Şifreleme veya geri çözme süreçlerinde 1 bitlik bile veri kaybı olursa pipeline çöker.-name: Run Cryptographic Migration Testenv:NODE_ENV: testDATABASE_URL:"file:./test.db"ENCRYPTION_KEY:"github-actions-secure-test-key-32-bytes"# Test ortamı için güvenli anahtarGEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}# Hassas anahtarlar GitHub Secrets'tan okunurrun:|cd apps/backendnpx prisma migrate deploynpm run db:migrate-crypto# 8. Adım: Başarı Bildirimi (Opsiyonel)-name: Pipeline Status Successif: success()run: echo"🎉 Tüm kod standartları, derleme testleri ve kriptografik veri göçü doğrulamaları başarıyla tamamlandı!"
Yapay zeka servisinin uç noktalarını test ederken veya ilerleyen aşamalarda canlı ortama geçiş yaparken API anahtarınızın sızmaması için:
GitHub üzerinde projenizin repository sayfasına gidin.
Settings -> Secrets and variables -> Actions adımlarını takip edin.
New repository secret
butonuna tıklayarak GEMINI_API_KEY
adıyla API anahtarınızı tanımlayın.
***********************************************************************************************************
Gönderdiğiniz müfredat dosyasını belirttiğiniz kurallar çerçevesinde (dakika/saniye bilgilerini temizleyerek, "ders" satırlarını atarak, anlamsız ifadeleri eleyerek) satır satır inceledim ve detaylı bir eğitim planı olarak tasarladım.
Dosya oldukça uzun ve teknik detaylar içerdiği için, yönergeniz doğrultusunda özet yapmadan, bol örnek, mimari açıklama ve uygulama pratikleriyle parça parça ilerleyeceğiz. İlk bölümde "Öğrenecekleriniz" kısmının derinlemesine analiziyle başlıyoruz.
Bu bölüm, geleneksel WordPress geliştiriciliği ile modern JavaScript dünyasını birleştiren Headless (Kafasız) CMS mimarisinin temelini oluşturur. Amaç, WordPress'i sadece bir veritabanı ve yönetim paneli (Backend) olarak kullanmak; kullanıcıya gösterilen ön yüzü (Frontend) ise Next.js ile tamamen bağımsız, ultra hızlı ve güvenli bir şekilde inşa etmektir.
Açıklama: Sektörde sadece standart WordPress teması yapan veya sadece statik React siteleri kuran binlerce geliştirici var. Bu eğitim size iki dev ekosistemi (WordPress + Next.js) birleştirme gücü katıyor. Kurumsal firmalar, içerik yönetim kolaylığı yüzünden WordPress'ten vazgeçemezler ancak hız ve SEO yüzünden Next.js isterler. Bu yetkinlik sizi piyasada aranan ve yüksek bütçeli projeler alan bir geliştiriciye dönüştürür.
Açıklama: React üzerine kurulu olan Next.js; yönlendirme (routing), performans optimizasyonları ve farklı render etme stratejilerini (SSR, SSG, ISR) hazır olarak sunan bir framework'tür. Bu eğitimde Next.js'in hem eski (Pages Router) hem de modern (App Router) mimarisini öğreneceksiniz.
Açıklama: SSG (Static Site Generation) mimarisidir. Web sitesindeki sayfalar, kullanıcı siteyi her ziyaret ettiğinde sıfırdan oluşturulmaz. Next.js, WordPress'teki verileri GraphQL ile çeker ve daha derleme (build) aşamasında tüm HTML sayfalarını hazırlar. Kullanıcı siteye girdiğinde anında yüklenen, sunucu yükü sıfıra yakın statik sayfalarla karşılaşır.
Örnek: Bir emlak sitesinde 1000 tane ilan varsa, bu ilanların tamamı saniyeler içinde açılacak şekilde statik HTML olarak önbelleğe alınır.
Açıklama: Standart WordPress REST API'si hantaldır; bir sayfayı çağırdığınızda ihtiyacınız olmayan yüzlerce satır veri de beraberinde gelir. GraphQL ise sadece "istediğiniz veriyi" almanızı sağlar. GraphiQL ise tarayıcı üzerinden bu sorguları yazıp test edebileceğiniz görsel bir arayüzdür.
Uygulama Pratiği (Sorgu Örneği): WordPress'ten sadece ana sayfadaki başlığı ve öne çıkan görseli istemek için yazacağımız sorgu şuna benzer:
queryGetHomepage {page(id:"home", idType: URI) {titlefeaturedImage {node {sourceUrl}}}}
Açıklama: Müşteriniz veya içerik yöneticiniz tanıdığı, bildiği WordPress paneline girecek; yeni bir sayfa veya emlak ilanı ekleyecek. Ancak bu içerik WordPress temasıyla değil, Next.js tarafında yazdığınız React bileşenleri (Components) tarafından yakalanıp ekrana basılacak. WordPress burada sadece bir "Veri Fabrikası" görevi görür.
Açıklama: Sitenizi Vercel platformuna dağıtacaksınız. WordPress panelinde bir yazı güncellendiğinde veya yeni bir ev ilanı eklendiğinde, Next.js sitesinin otomatik olarak tetiklenip güncel veriyi çekerek kendini yenilemesi gerekir. Buna Webhook mimarisi denir. Böylece site her zaman güncel kalır.
Açıklama: CSS dosyalarıyla boğuşmak yerine, HTML/React elementlerinin içine doğrudan sınıflar (class) yazarak hızlıca modern arayüzler tasarlamayı öğreneceksiniz. Tailwind CSS, kullanılmayan kodları temizlediği için sitenin performansını maksimumda tutar.
Açıklama: WordPress'in modern yazı editörü Gutenberg'tir. ACF Pro (Advanced Custom Fields) kullanarak, WordPress paneline tamamen kendimize özel içerik blokları (örneğin: Fiyat Tablosu, Emlak Özellik Listesi, Kaydıraklı Galeri) eklemeyi ve bunları Next.js'e aktarmayı öğreneceksiniz.
Açıklama: Emlak sitesindeki dinamik filtreleme (Fiyat aralığı, oda sayısı, konum seçimi gibi) işlemlerinde, sitenin durumunu (state) tüm sayfa boyunca yönetmek için React Context ve özel fonksiyonlar (Custom Hooks) yazarak temiz bir kod mimarisi kuracaksınız.
Açıklama: Next.js sadece
bir ön yüz aracı değildir. Kendi içinde pages/api
veya app/api
altında backend kodları çalıştırmanıza izin verir. Emlak
arama motorunda anlık arama (canlı filtreleme) yaparken
WordPress'e doğrudan yüklenmek yerine, Next.js API rotalarını
bir köprü olarak kullanacaksınız.
Müfredatın ilk teknik uygulama adımları, WordPress Gutenberg editöründeki blok verilerini temizlemek ve bunları Next.js bileşenlerine eşlemek üzerine kuruludur.
WordPress'te sayfalar artık düz metin değil, bloklar halinde tutulur (Başlık bloğu, paragraf bloğu, resim bloğu vb.). Next.js tarafında bu blokları render edebilmek için öncelikle sayfanın ham blok verilerini GraphQL üzerinden talep etmeliyiz.
Uygulama Tasarımı:
WordPress sorgumuzda blocks
alanını isteyeceğiz. Bu alan bize sayfadaki tüm blokların JSON
formatındaki listesini döndürecek.
WordPress'ten gelen blok verileri genellikle çok karmaşıktır ve içinde gereksiz HTML etiketleri barındırır. Next.js projemizde tüm gelen blokları sırayla okuyup, ilgili React bileşenine yönlendirecek merkezi bir kontrol merkezi kuracağız. Buna BlockRenderer denir.
Uygulama (Kod Örneği):
// components/BlockRenderer.jsimport { Cover } from "./Cover";import { Heading } from "./Heading";import { Paragraph } from "./Paragraph";export function BlockRenderer({ blocks }) {return blocks.map((block, index) => {switch (block.name) {case "core/cover":return <Cover key={index} src={block.attributes.url} title={block.attributes.title} />;case "core/heading":return <Heading key={index} level={block.attributes.level} content={block.attributes.content} />;case "core/paragraph":return <Paragraph key={index} content={block.attributes.content} />;default:return null; // Tanımlanmayan blokları güvenle atla}});}
Emlak sitemizin en üstünde büyük, görkemli bir arka plan resmi ve üzerinde bir başlık olacak (Cover Bloğu).
Tasarım İlkesi:
Performans için standart <img>
etiketi yerine Next.js'in <Image
/> bileşenini kullanacağız. Bu bileşen
resimleri otomatik olarak WebP formatına çevirir, boyutlandırır
ve tembel yükleme (lazy loading) uygular. Tailwind CSS ile resmi
tam ekran yapıp üzerine siyah bir katman (overlay) ekleyeceğiz.
Ana WordPress temanız
güncellendiğinde yaptığınız kod değişikliklerinin
kaybolmaması için bir Child Theme
oluşturacağız. Temamızın functions.php
dosyasına girerek, WordPress'in GraphQL API'sine (WPGraphQL) tam
uyumlu çalışmasını sağlayacak geliştirmeleri ekleyeceğiz.
Gutenberg'den gelen <h1>,
<h2>,
<h3>
gibi başlıkları dinamik olarak karşılayan bir React bileşeni
yazacağız. WordPress'in sunduğu özel renk paletlerini Tailwind
CSS'e entegre etmek için tailwind.config.js
dosyasını genişleteceğiz (extend). Böylece WordPress panelinden
seçilen bir kırmızı renk, Tailwind'deki karşılığı ile
eşleşecek.
Kullanıcı sitemizde
/satilik-villa
veya /hakkimizda
gibi bir linke tıkladığında, Next.js'in bunu dinamik olarak
yakalaması gerekir.
Uygulama Yapısı: Next.js
(Pages Router) içinde pages/[...slug].js
dosyası oluşturacağız. Bu dosya, adresten gelen url bilgisini
alır, WordPress'e "Bu URL'e sahip bir sayfa var mı?"
diye sorar ve sayfayı dinamik olarak inşa eder.
Next.js'in sihirli
fonksiyonlarından biri getStaticProps'tur.
Sayfa yayına alınmadan önce (derleme anında) çalışan bu
fonksiyon, WordPress mimarisine bağlanıp ilgili sayfanın içeriğini
çeker ve sayfaya enjekte eder. Siteniz bu sayede veritabanı
bağlantısı kopsa bile ayakta kalır ve jet hızında açılır.
WordPress editöründe yazılan
paragrafların içindeki linkler genellikle ://ekran.com...
veya doğrudan WordPress'in kendi URL'ine yönlenir. Eğer kullanıcı
bu linklere tıklarsa siteden çıkabilir veya sayfa yavaşça
sıfırdan yüklenebilir.
Uygulama Pratiği: Paragraf
bileşeni içinde gelen tüm <a>
etiketlerini regex veya bir parser yardımıyla tarayacağız. Eğer
link site içi bir link ise, bunu Next.js'in hızlı sayfa geçişi
sağlayan <Link>
bileşenine dönüştüreceğiz. Böylece sayfa yenilenmeden,
uygulama içi yumuşak geçiş sağlanacak.
Harika! O halde hiç hız kesmeden, mimari detayları ve kod yapılarını koruyarak müfredatın bir sonraki aşaması olan Ana Menü Tasarımı ve Gelişmiş Gutenberg Blokları ile devam ediyoruz.
Bir web sitesinin en kritik bileşenlerinden biri dinamik menü yönetimidir. Klasik WordPress temalarında menüler sürükle-bırak yöntemiyle yapılır. Headless mimaride ise bu menü verisini WordPress'ten çekip, Next.js tarafında performanslı ve erişilebilir bir bileşene dönüştürmemiz gerekir.
WordPress'in varsayılan menü sistemi GraphQL ile her zaman esnek çalışmayabilir. Bu yüzden ACF Pro (Advanced Custom Fields) kullanarak WordPress paneline "Tema Ayarları" adında özel bir seçenekler sayfası (Options Page) ekleyeceğiz.
Tasarım İlkesi: Bu sayfada bir Repeater (Tekrarlayıcı) alan oluşturacağız. Her menü elemanı için:
Etiket (Label): Menüde görünecek yazı (Örn: "Hakkımızda").
Hedef (Destination): Sayfa linki veya harici URL.
Alt Menü Elemanları (Sub-menu items): Açılır menü (Dropdown) desteği için iç içe başka bir tekrarlayıcı alan.
WordPress panelinden girilen menü verisi, GraphQL sorgusuyla hiyerarşik bir JSON formatında Next.js'e gelir. Bu veriyi doğrudan kullanmadan önce, front-end tarafında işimizi kolaylaştıracak temiz bir dizi (array) yapısına dönüştüreceğiz.
GraphQL Menü Sorgusu Örneği:
queryGetMainMenu {acfOptionsPage {mainMenuFields {menuItems {labeldestination {...onPage {uri}}subMenuItems {labeldestination {...onPage {uri}}}}}}}
Menü bileşeni, her sayfada görüneceği için ortak bir düzende (Layout) yer almalıdır. Masaüstünde yan yana dizilen, mobilde ise açılır bir hamburger menüye dönüşen responsive bir yapı tasarlayacağız.
Uygulama Pratiği: Alt
menülerin (dropdown) açılıp kapanma durumlarını yönetmek için
React'ın useState
hook'undan yararlanacağız. Fare üzerine geldiğinde
(onMouseEnter)
veya tıklandığında alt menüyü görünür kılacağız.
Emlak sitelerinde genellikle menünün en sağında dikkat çekici bir "İlan Ekle" veya "Bize Ulaşın" butonu bulunur. Menü verimize bir boolean (doğru/yanlış) alanı ekleyerek, ilgili menü elemanının normal bir link mi yoksa vurgulanmış bir CTA butonu mu olacağını belirleyeceğiz.
@apply
Direktifi
Tekrar kullanılabilir bir buton
bileşeni (ButtonLink)
oluşturacağız. Tailwind CSS sınıflarını temiz tutmak ve global
buton stilleri yaratmak için projenin global CSS dosyası içerisinde
@apply
yönergesini kullanacağız.
Uygulama (CSS Örneği):
/* styles/globals.css */.btn-primary {@applybg-blue-600 text-white px-4 py-2 rounded-lg font-medium transition-colors hover:bg-blue-700;}.btn-secondary {@applyborder border-gray-300 text-gray-700 px-4 py-2 rounded-lg font-medium hover:bg-gray-50;}
React tarafında ise <Link
className="btn-primary" href={destination}> {label}
</Link>
şeklinde kullanacağız.
Menü ve alt bilgi (footer) gibi
her sayfada tekrar eden alanları her sayfa bileşeninin içine tek
tek eklemek yerine, merkezi bir <Layout>
bileşeni oluşturacağız. pages/_app.js
veya dinamik sayfalarımızı bu Layout ile sarmallayarak kod
tekrarını önleyeceğiz.
WordPress'in gücü, sayfa düzenini esnek şekilde değiştirebilmekten gelir. Bu bölümde ACF Pro kullanarak WordPress editörüne tamamen sıfırdan, kendimize özel bloklar enjekte etmeyi öğreneceğiz.
WordPress'e php kullanarak yeni bir
Gutenberg bloğu kaydedeceğiz (acf_register_block_type).
Bu blok, editör içinde zengin bir arka plan rengi, bir başlık ve
bir buton içerecek. İçerik üreticisinin WordPress panelinde ne
yaptığını görebilmesi için bir Block Preview (Blok
Önizleme) şablonu
hazırlayacağız.
WordPress panelinde doldurulan bu
CTA bloğu verileri, BlockRenderer
bileşenimize acf/cta-block
ismiyle düşecek. Bu veriyi karşılayacak şık bir React bileşeni
yazacağız.
Uygulama (Kod Örneği):
// components/CtaBlock.jsimport { ButtonLink } from "./ButtonLink";export function CtaBlock({ heading, text, buttonLabel, buttonDestination }) {return (<div className="bg-slate-100 p-8 my-6 rounded-2xl text-center max-w-4xl mx-auto shadow-sm"><h3 className="text-2xl font-bold text-slate-800 mb-2">{heading}</h3><p className="text-slate-600 mb-6">{text}</p><ButtonLink variant="primary" href={buttonDestination}>{buttonLabel}</ButtonLink></div>);}
Gutenberg'in en güçlü özelliklerinden biri, bir bloğun içine başka bloklar yerleştirebilmektir (Örn: Yan yana 3 kolon oluşturup, her kolonun içine farklı resimler ve paragraflar koymak).
Mimari Tasarım: Next.js
tarafında core/columns
(Ana kapsayıcı) ve core/column
(Her bir sütun) bloklarını karşılayacak bir yapı kuracağız.
core/column
bileşeni kendi içinde yeniden BlockRenderer
çağırarak iç içe geçen (recursive) bir render döngüsü
oluşturacak. Böylece kaç katman iç içe blok olursa olsun
Next.js bunu hatasız çözebilecek.
Gutenberg içindeki standart resim
blokları, Next.js'e ham HTML veya düz URL olarak gelir. Kolonların
responsive (mobil uyumlu) yapısını bozmamak için, gelen resim
verilerini yakalayıp Tailwind'in grid
grid-cols-1 md:grid-cols-3
gibi esnek yapılarıyla saracağız ve Next.js <Image
/>
bileşenine fill
özelliği vererek üst kapsayıcısının boyutunu otomatik almasını
sağlayacağız.
WordPress'te bir kere hazırlanan bir blok düzeni (örneğin standart bir form veya iletişim alanı) "Yeniden Kullanılabilir Blok" olarak kaydedilebilir. GraphQL sorgularımızda bu ortak blokların ID'lerini takip ederek, Next.js tarafında tek bir istekte tüm içeriği tek seferde çekebilecek bir eşleme algoritması kuracağız.
Harika! Şimdi projenin en kapsamlı ve fonksiyonel aşaması olan Property Pages (Emlak / Gayrimenkul İlan Sayfaları ve Gelişmiş Arama Motoru) bölümüne geçiyoruz.
Bu bölümde sadece statik sayfalar üretmeyeceğiz; Next.js üzerinde çalışan, filtreleme yeteneğine sahip dinamik bir arama motoru API'si inşa edeceğiz.
Bir emlak sitesinin kalbi, ilanların detay sayfaları ve kullanıcıların fiyat, oda sayısı gibi kriterlere göre ev arayabileceği filtreleme mekanizmasıdır.
WordPress'te ilanları yönetmek için varsayılan "Yazılar" (Posts) yerine Custom Post Type (Özel Yazı Türü) kullanarak "Properties" adında yeni bir ilan yönetim alanı açacağız.
ACF Pro Entegrasyonu: Her ilan için şu özel alanları (Custom Fields) tanımlayacağız:
Fiyat
(Price): Sayısal veri (Örn: 4500000).
Yatak
Odası Sayısı (Bedrooms): Sayısal veri (Örn: 3).
Banyo
Sayısı (Bathrooms): Sayısal veri (Örn: 2).
Otopark Var mı? (Has Parking): True/False seçeneği.
Her bir ilanın kendine özel bir
URL'i olmalıdır (Örn: /properties/bogaz-manzarali-lu-ks-villa).
Mimari
Tasarım: Next.js (Pages Router) mimarisinde
pages/properties/[propertySlug].js
dosyasını oluşturacağız.
Derleme Stratejisi:
getStaticPaths
fonksiyonu ile WordPress'teki tüm ilanların listesini ve
URL'lerini (slug) çekeceğiz. Next.js, sitenin derlenme aşamasında
tüm bu ilan sayfalarını tek tek statik HTML olarak üretecektir.
İlan detay sayfasına girildiğinde
en üstte evin devasa bir fotoğrafı (Featured
Image)
ve üzerinde ilan başlığı yer alacak.
Uygulama: WordPress'ten
gelen öne çıkan görsel URL'ini alıp, daha önce hazırladığımız
<Cover>
bileşenine dinamik arka plan resmi olarak paslayacağız.
İçerik yöneticisinin, ana sayfa
dahil istediği herhangi bir sayfaya emlak arama motorunu
yerleştirebilmesini istiyoruz. Bunun için WordPress'te Property
Search
adında içi boş, sadece bir belirteç (placeholder) görevi gören
bir ACF Bloğu kaydedeceğiz. Next.js bu bloğu gördüğü an, oraya
gelişmiş arama arayüzümüzü (formlar, filtreler ve sonuç
listesi) monte edecek.
Kullanıcılar "Fiyatı 5 Milyon TL'den az olan 3+1 evleri listele" dediğinde, her seferinde tüm sayfayı yenilemek performansı düşürür. Bunun yerine arka planda çalışacak bir API yazacağız.
Dosya
Konumu: pages/api/search.js
Çalışma Mantığı: Bu
API'ye bir POST
veya GET
isteği atılacak. API, gelen filtre parametrelerini alıp WordPress
GraphQL sunucusuna iletecek ve süzülmüş ham veriyi temiz bir
şekilde ön yüze döndürecek.
API'den dönen ev listesini (başlık, fiyat, görsel, oda sayısı), Tailwind CSS ile tasarlanmış şık kartlar (Card Component) halinde grid düzeninde ekrana basacağız.
Eğer sitede 500 tane ilan varsa, hepsini aynı anda yüklemek siteyi kilitler. Bu yüzden her sayfada 9 ilan gösterecek şekilde bir sayfalama sistemi kuracağız.
UI Tasarımı: İlanların altında "Önceki", "Sonraki" ve sayfa numaralarının olduğu butonlar yer alacak.
API Mantığı: search.js
API'sine page
parametresi göndereceğiz (Örn: /api/search?page=2).
API, WordPress tarafına GraphQL'in after
(Cursor) özelliğini göndererek sadece sıradaki 9 evi çekecek.
Kullanıcının seçim yapabileceği select kutuları ve girdiler tasarlayacağız.
Uygulama Pratiği: Form
elemanlarının güncel değerlerini anlık olarak takip etmek için
React'ın useState
yapısını veya tüm filtreleri tek bir yerde toplamak için bir
nesne (object) durumunu kullanacağız.
// components/PropertySearch/Filters.jsimport { useState } from "react";export function Filters({ onSearch }) {const [bedrooms, setBedrooms] = useState("");const [maxPrice, setMaxPrice] = useState("");const handleApplyFilters = () => {// Üst bileşendeki arama fonksiyonunu tetikleonSearch({ bedrooms, maxPrice });};return (<div className="flex gap-4 p-4 bg-white shadow-md rounded-xl mb-6"><selectvalue={bedrooms}onChange={(e) => setBedrooms(e.target.value)}className="border p-2 rounded"><option value="">Oda Seçin</option><option value="1">1 Oda</option><option value="2">2 Oda</option><option value="3">3+ Oda</option></select><inputtype="number"placeholder="Maksimum Fiyat"value={maxPrice}onChange={(e) => setMaxPrice(e.target.value)}className="border p-2 rounded"/><button onClick={handleApplyFilters} className="bg-blue-600 text-white px-4 py-2 rounded">Filtrele</button></div>);}
Kullanıcı filtreleme yaptığında,
tarayıcıdaki URL adres çubuğunun da güncellenmesini istiyoruz
(Örn: /properties/search?bedrooms=3&maxPrice=5000000).
Neden Önemli? Kullanıcı bu aramayı yaptıktan sonra linki kopyalayıp bir arkadaşına gönderdiğinde, arkadaşının da aynı filtrelenmiş sonuçları görmesi gerekir.
Çözüm: Next.js useRouter
(veya yeni mimaride useSearchParams)
kullanarak filtreler değiştikçe sayfayı yenilemeden URL'i
güncelleyen (router.push(...,
undefined, { shallow: true })) bir mekanizma
entegre edeceğiz.
Bu bölüm sitenin kurumsallığını artıran, form yönetimi, galeri sistemleri ve SEO gibi ince işçilik gerektiren bileşenlerin eklendiği aşamadır.
Emlak sayfalarında "Acenteyle İletişime Geçin" formu için backend kodlarıyla uğraşmamak adına popüler bir form yönetim servisi olan Formspree kullanacağız.
WordPress
Tarafı: Formspree
Form adında bir Gutenberg bloğu açıp form
ID'sini alacağız.
Next.js Tarafı:
Formspree'nin resmi React kütüphanesini (@formspree/react)
kullanarak, kullanıcının girdiği ad, e-posta ve mesajı doğrudan
Formspree üzerinden e-posta adresimize yönlendiren güvenli bir
form bileşeni inşa edeceğiz.
Evin kaç metrekare olduğu, cephesi, ısıtma durumu gibi teknik detayları listelemek için esnek bir blok oluşturacağız. WordPress'ten gelen bu teknik listeyi Next.js tarafında ikonlar eşliğinde temiz bir tablo veya grid düzeniyle sergileyeceğiz.
İlanların içine eklenecek çoklu fotoğraflar için Gutenberg'in yerleşik Galeri bloğunu yakalayacağız. Next.js tarafında fotoğrafların üzerine tıklandığında ekranı kaplayan, sağa-sola kaydırılabilen modern bir Lightbox (Lightbox Gallery) bileşeni yazacağız.
WordPress editöründe bir kolona
arka plan rengi veya yazı rengi seçildiğinde, WordPress bu
sınıfları has-background
veya has-dark-gray-color
gibi isimlerle çıktı verir.
Refaktör İşlemi: Next.js
tarafındaki Column
bileşenimizi bu renk sınıflarını okuyacak şekilde
güncelleyeceğiz. Gelen WordPress renk isimlerini Tailwind CSS'teki
karşılıklarıyla (bg-slate-800,
text-gray-100
vb.) eşleştiren dinamik bir yardımcı fonksiyon (util
function) yazacağız.
Özellikle avantajların listelendiği alanlarda ("Depreme dayanıklı", "Krediye uygun") satırların başına yeşil onay işareti koyabilmek için mikro bir Gutenberg bloğu tasarlayıp React'ta render edeceğiz.
Harika! Şimdi kursun en kritik dönüm noktasına geliyoruz: Next.js 13.4+ Güncellemesi ve App Router (App Directory) Geçişi.
Bu bölüm, projenin eski mimariden
(pages
klasörü) modern, bileşen odaklı ve performans canavarı olan yeni
mimariye (app
klasörü) taşındığı, profesyonel refaktör süreçlerini
içerir. Sonrasında ise projemizi canlıya alıp optimizasyonlarını
tamamlayacağız.
Next.js 13.4 ile birlikte gelen App Router mimarisi, web geliştirmede köklü bir değişiklik yaratmıştır. Artık her bileşen varsayılan olarak bir Server Component (Sunucu Bileşeni)'dir. Bu da tarayıcıya neredeyse hiç JavaScript göndermeyerek sitenin anında yüklenmesini sağlar.
İlk olarak package.json
dosyasındaki Next.js, React ve Tailwind CSS sürümlerini
güncelleyeceğiz. Ardından projenin kök dizinine (root) yeni app
klasörünü açarak geçiş hazırlıklarını tamamlayacağız.
Eski mimarideki pages/index.js
ve pages/[...slug].js
dosyalarımızı yeni düzene taşıyoruz.
Yeni Yapı:
pages/index.js
→ app/page.js
(Ana sayfa)
pages/[...slug].js
→ app/[...slug]/page.js
(Dinamik sayfalar)
pages/_app.js
& _document.js
→ app/layout.js
(Kök Düzen / Root Layout)
Yeni mimaride en çok dikkat
etmemiz gereken kural budur. WordPress'ten veri çeken ve statik
olarak basılan tüm bileşenleri Server Component
olarak bırakacağız. Ancak arama motoru filtreleri, buton
tıklamaları gibi kullanıcı etkileşimi (state, event listener)
gerektiren yerlerin en üstüne "use
client";
ifadesini ekleyerek bunları Client Component'e
dönüştüreceğiz.
Yeni layout.js
dosyasında, Next.js'in performans optimize edilmiş yerleşik font
sistemini (next/font/google)
kullanarak Google Yazı Tiplerini projeye dahil edeceğiz. Küresel
CSS dosyamızı buraya import ederek Tailwind'i aktaracağız.
FontAwesome ikonlarının sayfa yüklenirken büyümesini engellemek
için CSS kurallarını sabitleyeceğiz.
Eski mimaride her sayfa geçişinde
menü bileşeni sarsılabiliyor veya tekrar render edilebiliyordu.
Yeni app/layout.js
içerisine <MainMenu
/>
bileşenimizi yerleştiriyoruz. Böylece kullanıcı sayfalar
arasında gezinirken menü asla baştan yüklenmez, sadece değişen
sayfa içeriği (children) güncellenir.
generateMetadata
Fonksiyonu ile Dinamik SEO Yönetimi
Eski mimaride kullandığımız
next/head
yapısı artık geride kaldı. App Router'da SEO verilerini (Başlık,
Açıklama, Open Graph görselleri) yönetmek için dışa aktarılan
(export) özel bir fonksiyon kullanırız.
Uygulama: WordPress'ten GraphQL ile gelen SEO verilerini bu fonksiyon içinde yakalayıp Next.js'e teslim edeceğiz.
// app/[...slug]/page.jsexportasyncfunctiongenerateMetadata({params}) {constslug= params.slug.join('/');constpageData=awaitgetPageData(slug);// WordPress'ten SEO verisini çekreturn{title: pageData.seo.title ||"Emlak Sitesi",description: pageData.seo.description,openGraph: {images: [pageData.seo.opengraphImage?.sourceUrl],},};}
next/navigation)
ve API Rotalarının Güncellenmesi
Yeni mimaride next/router
modülü çalışmaz. Emlak arama motorumuz içindeki yönlendirmeleri
ve URL güncellemelerini next/navigation
içinden çekeceğimiz useRouter,
usePathname
ve useSearchParams
hook'ları ile baştan yazacağız.
Route Handlers: Eski
pages/api/search.js
dosyamızı modern Route Handler yapısına dönüştürerek
app/api/search/route.js
konumuna taşıyacağız ve export
async function POST(request) {...} şeklinde
tanımlayacağız.
Next.js App Router, arkada çok
agresif bir önbellekleme sistemi çalıştırır. WordPress'ten veri
çekerken kullandığımız fetch
isteklerine özel parametreler geçerek verinin ne kadar süre
yayında kalacağını belirleyeceğiz. (Örn: fetch('...',
{ next: { revalidate: 60 } })
→ Veriyi 60 saniyede bir tazele).
Projemiz yerel bilgisayarımızda kusursuz çalıştığına göre, artık tüm dünyanın erişebileceği bulut sunucularına taşınma vaktidir.
WordPress: Tercihen hızlı ve güvenli bir bulut sunucuya (veya Headless uyumlu hosting firmalarına) taşınır.
Next.js: Projenin GitHub deposu (repository) oluşturulur ve kodlar push edilir. Ardından Vercel paneline girilerek GitHub projesi bağlanır. WordPress'in GraphQL URL'i, Vercel paneline bir çevre değişkeni (Environment Variable - NEXT_PUBLIC_WORDPRESS_URL) olarak güvenle eklenir ve site canlıya alınır.
Sitemiz statik ve önbelleğe alınmış olduğu için, WordPress panelinde bir evin fiyatı değiştirildiğinde bu değişiklik Next.js sitesinde anında görünmez. Sitenin tamamını dakikalarca baştan derlemek (rebuild) yerine akıllı bir tetikleyici kuracağız.
Eski Mimari
(Pages klasörü için): WordPress'e bir eklenti veya webhook
kuracağız. WordPress'te bir ilan güncellendiğinde, Next.js
sitemizdeki pages/api/revalidate.js
adresine gizli bir şifre (token) ile istek atılacak. Next.js
sadece o güncellenen sayfanın önbelleğini saniyeler içinde
temizleyecek.
Yeni Mimari (App klasörü
için): Route Handler kullanarak app/api/revalidate/route.js
rotasını tasarlayacağız. WordPress'ten tetikleme geldiğinde
revalidatePath('/properties/ilgili-ilan')
veya revalidateTag('wordpress-data')
fonksiyonunu çağırarak önbelleği milisaniyeler içinde çöpe
atıp güncel verinin ekrana yansımasını sağlayacağız.
Giriş: İçerik yöneticisi WordPress paneline girer, ACF ile tasarladığımız blokları kullanarak lüks bir villa ilanı ekler ve kaydeder.
Tetiklenme: WordPress arkada çalışan Webhook mekanizması sayesinde Vercel'deki Next.js API rotamıza "Veri değişti!" sinyali gönderir.
Derleme &
Önbellek: Next.js, GraphQL üzerinden yeni ilanın verilerini
ve blok düzenini saniyeler içinde çeker, BlockRenderer
ile React bileşenlerine dönüştürür ve statik HTML olarak
sunucuya yazar.
Kullanıcı Deneyimi: Müşteri siteye girdiğinde, veritabanı sorgularıyla vakit kaybetmeyen, Tailwind ile optimize edilmiş, Next.js Image mimarisiyle resimleri sıkıştırılmış, saniyenin onda biri hızında açılan bir web sitesiyle karşılaşır. Arama motorunu kullandığında ise Next.js API rotaları sayesinde sayfayı yenilemeden milisaniyeler içinde ilanları filtreleyebilir.
**********************************************************