Portfolyo Sitesinde Uzun Vadede Fark Yaratan Mimari Kararlar
Çoğu portfolyo sitesi tek bir an için inşa edilir: iş başvurusu veya müşteri sunumu. Geliştiriciler bir şeyi hayata geçirmek için aceleyle öğrendikleri son framework'ü kullanır ve gönderir. Altı ay sonra ise yeni bir bölüm eklemek istediklerinde kendi kodlarıyla boğuşuyorlar.
gucluyumhe.dev'in farklı olmasını istedim. Bugün iyi görünen bir portfolyo değil, minimum bakım yükü ile zamanla iyileşen bir teknik platform.
İşte en büyük uzun vadeli farkı yaratan 6 mimari karar — ve akıllıca görünen ama sürtünme yaratan 2 karar.
Karar 1: İçerik için Tek Gerçek Kaynak Olarak Markdown
Cazip Olan: CMS veya Veritabanı Kullanmak
Pek çok geliştirici blog içeriği için içgüdüsel olarak headless bir CMS'e (Contentful, Sanity, Notion API) veya veritabanına (Supabase, PlanetScale) uzanır. Bunlar ölçeklenebilir ve production'a hazır hissettiriyor.
Tek yazarlı bir portfolyo blogu için bu muazzam bir aşırı mühendisliktir.
Bunun Yerine Ne Yaptım: Dosya Sistemi Markdown
Her blog yazısı /content/posts/ içinde bir .md dosyası. Slug dosya adı, metadata frontmatter. Tüm içerik kataloğu, klonlayabileceğiniz ve herhangi bir metin editöründe açabileceğiniz bir dizin.
// src/utils/markdown.ts
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import remarkHtml from 'remark-html';
export interface PostFrontmatter {
title: string;
date: string;
author: string;
excerpt: string;
coverImage: string;
tags: string[];
category: string;
locale: 'en' | 'tr';
}
export interface PostData extends PostFrontmatter {
slug: string;
contentHtml: string;
readingTimeMinutes: number;
}
const POSTS_DIRECTORY = path.join(process.cwd(), 'content/posts');
export async function getPostBySlug(slug: string): Promise<PostData | null> {
try {
const fullPath = path.join(POSTS_DIRECTORY, `${slug}.md`);
const fileContents = await fs.readFile(fullPath, 'utf8');
const { data, content } = matter(fileContents);
const processedContent = await remark()
.use(remarkHtml, { sanitize: false })
.process(content);
const wordCount = content.split(/\s+/).length;
return {
slug,
...(data as PostFrontmatter),
contentHtml: processedContent.toString(),
readingTimeMinutes: Math.ceil(wordCount / 200)
};
} catch {
return null;
}
}
Neden Uzun Vadede Kazandı?
- Sıfır bağımlılık: CMS aboneliği yok, API rate limiti yok, vendor bağımlılığı yok
- Git geçmişi changelog'unuz: Her düzenleme yazar ve zaman damgasıyla version-controlled
- Offline-first geliştirme: İnternetsiz, herhangi bir editörde yazı yazın
- Build-time rendering: Markdown build anında ayrıştırılır — sıfır runtime yükü
- Konvansiyonla çok dilli:
post.md(İngilizce) vepost_tr.md(Türkçe), dosya adından dil tespiti
Üç yıl sonra Next.js'ten Astro veya SvelteKit'e geçersem, tüm içerik katalogum sıfır değişiklikle migrate oluyor. Markdown dosyaları framework-agnostik.
Karar 2: Çeviri Kütüphanesi Olmadan Locale Tabanlı Routing
Cazip Olan: next-i18next veya next-intl
Çoğu çok dilli Next.js tutorial'ı hemen next-i18next veya next-intl'e uzanır. Bunlar güçlü kütüphaneler — ama basit bir kullanım durumu için karmaşıklık, konfigürasyon dosyaları ve anlaşılması zor bir mental model ekliyor.
Ne Yaptım: Konvansiyon Tabanlı Routing
// app/[locale]/blog/[slug]/page.tsx
export async function generateStaticParams() {
const allFiles = await fs.readdir(POSTS_DIRECTORY);
return allFiles
.filter(file => file.endsWith('.md'))
.map(file => {
const isTranslation = file.endsWith('_tr.md');
const locale = isTranslation ? 'tr' : 'en';
const slug = file
.replace('_tr.md', '')
.replace('.md', '');
return { locale, slug };
});
}
content/posts/
building-an-ai-agent-with-mcp.md adresi: /en/blog/building-an-ai-agent-with-mcp
building-an-ai-agent-with-mcp_tr.md adresi: /tr/blog/building-an-ai-agent-with-mcp
nextjs-performance-before-seo.md adresi: /en/blog/nextjs-performance-before-seo
nextjs-performance-before-seo_tr.md adresi: /tr/blog/nextjs-performance-before-seo
Bu karar bir bağımlılığın tamamını ve yüzlerce satır konfigürasyonu ortadan kaldırdı. Yeni bir dil eklemek _de.md suffix'li dosyalar eklemek demek — kütüphane konfigürasyon değişikliği yok.
Karar 3: Her Sayfa Türü için JSON-LD Structured Data
Cazip Olan: Atlamak — Arama Motorları Halleder
Çoğu geliştirici JSON-LD'yi ertelenmiş sonuçlarla ekstra SEO çalışması gibi hissettirdiği için atlar. 2026'da bu iki nedenle hata:
- Geleneksel SEO: Google açıkça structured data'yı rich snippet'lar, yazar doğrulaması ve sıralama sinyalleri için kullanıyor
- GEO (Generative Engine Optimization): Gemini, Claude ve Perplexity gibi AI modelleri yazar güvenilirliğini ve gerçek atıfı doğrulamak için structured data çekiyor
Ne Uyguladım?
export function generateBlogPostSchema(post: PostData, url: string) {
return {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": post.title,
"description": post.excerpt,
"image": post.coverImage,
"datePublished": post.date,
"dateModified": post.date,
"author": {
"@type": "Person",
"name": "Ömer Özbay",
"url": "https://gucluyumhe.dev",
"sameAs": [
"https://github.com/gucluyumhe",
"https://linkedin.com/in/omerozbay"
]
},
"publisher": {
"@type": "Person",
"name": "Ömer Özbay",
"url": "https://gucluyumhe.dev"
},
"keywords": post.tags.join(", "),
"inLanguage": post.locale === 'tr' ? "tr-TR" : "en-US",
"url": url
};
}
export default async function BlogPostPage({ params }: Props) {
const post = await getPostBySlug(params.slug);
const url = `https://gucluyumhe.dev/${params.locale}/blog/${params.slug}`;
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBlogPostSchema(post, url))
}}
/>
<ArticleLayout post={post} />
</>
);
}
GitHub ve LinkedIn'e bağlanan sameAs dizisi GEO için kritik. AI modellerinin "Ömer Özbay"ı yalnızca bir ad dizisi değil, belirli bir varlık olarak ayrıştırmasına izin veriyor.
Karar 4: Edge-First Deployment (Vercel Edge Network)
Cazip Olan: Kontrol İçin VPS'de Self-Host
Vercel veya Netlify'ın kontrolden vazgeçmek olduğunu hisseden birçok geliştirici var. Bir portfolyo sitesi için bu yanlış bir değiş tokuş hesabı.
Bir Next.js uygulamasını self-host etmek şunları yönetmek anlamına gelir: SSL sertifikası yenileme, DDoS koruması, global CDN kurulumu, sıfır downtime deployment ve sağlık izleme.
Bir portfolyo için bu, sıfır değer üreten bakım yüküdür.
vercel.json'un Değeri
Tek bir konfigürasyon dosyası size global dağıtım, otomatik HTTPS ve güvenlik başlıkları dahil akıllı routing sağlar:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-XSS-Protection", "value": "1; mode=block" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
]
},
{
"source": "/fonts/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
],
"redirects": [
{ "source": "/blog/:slug", "destination": "/en/blog/:slug", "permanent": false }
]
}
Uzun vadeli kazanım: altyapıda sıfır bakım, otomatik ölçekleme ve yerleşik analitik.
Karar 5: Dinamik Site Haritası Oluşturma
Cazip Olan: Statik sitemap.xml
Statik bir sitemap.xml, yeni bir yazı yayınladığınızda manuel güncellemeler gerektirir. Üçüncü yazıda güncellemeyi unutursunuz. Onuncuda kalıcı olarak eski hale gelir.
Çözüm: Next.js App Router Sitemap
// app/sitemap.ts
import { MetadataRoute } from 'next';
import { getAllPosts } from '@/utils/markdown';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
const baseUrl = 'https://gucluyumhe.dev';
const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/${post.locale}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: 'monthly',
priority: 0.8,
}));
return [
{ url: baseUrl, lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
{ url: `${baseUrl}/en/blog`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.9 },
{ url: `${baseUrl}/tr/blog`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.9 },
...postEntries,
];
}
Yeni bir markdown dosyası ekleyin, site haritası bir sonraki build'de otomatik güncellenir. Sıfır bakım.
Karar 6: CSS'de Sorumlulukların Ayrılması
Çoğu Geliştiricinin Yaptığı
Her elementin üzerine satır içi Tailwind utility'leri, kod tabanında tutarsızlık yaratarak:
// Bakımı zor: stiller bileşenler arasında dağınık
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mt-8 mb-4 leading-tight tracking-tight">
Ne Yaptım: CSS Custom Properties ve Semantik Sınıflar
/* globals.css — tasarım token'ları custom property olarak */
:root {
--color-text-primary: #0f172a;
--color-text-secondary: #475569;
--color-accent: #6366f1;
--spacing-section: 4rem;
--font-size-h1: clamp(2rem, 5vw, 3.5rem);
--border-radius-card: 0.75rem;
}
[data-theme="dark"] {
--color-text-primary: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-accent: #818cf8;
}
/* Semantik bileşen sınıfları */
.article-title {
font-size: var(--font-size-h1);
font-weight: 700;
color: var(--color-text-primary);
line-height: 1.2;
letter-spacing: -0.02em;
margin-bottom: 1rem;
}
Uzun vadeli kazanım: tüm site genelinde vurgu rengini değiştirmek tek satır CSS değişikliği — 50 dosyada grep ve replace değil.
Acı Yaratan 2 Karar
Acı Veren Karar 1: Çok Erken Çok Fazla Sayfa Türü
Blog yazıları, proje vitrini, deneyim zaman çizelgesi, beceriler ızgarası, iletişim formu ve 404 için ayrı layout'lar oluşturdum. İlk versiyonda altı farklı layout türü.
Sonuç: her tasarım iterasyonu 6 dosyaya dokunmayı gerektirdi. Layout değişiklikleri bir angarya haline geldi.
Daha iyi yaklaşım: 2–3 genel layout shell ile başlayın. Uzmanlığı yalnızca içerik gerçekten gerektirdiğinde ekleyin.
Acı Veren Karar 2: Aynı Bileşende Dilleri Karıştırma
Başlangıçta bazı bileşenler her iki dilde hardcoded string içeriyordu:
// Bakım kabusu yarattı
<p>{locale === 'tr' ? 'Son yazılar' : 'Latest posts'}</p>
20'den fazla bileşende 15'ten fazla böyle string ile bu bir bakım kabusu haline geldi.
Daha iyi yaklaşım: i18n kütüphanesi olmadan bile, tüm görüntüleme string'lerini ilk günden itibaren locale-specific JSON dosyalarına çıkarın.
Mimarinin Özeti
gucluyumhe.dev Mimarisi (2026)
├── Icerik Katmani: Markdown dosyaları, framework-agnostik
├── Routing Katmani: Next.js App Router ([locale]/[slug])
├── Rendering Katmani: React Server Components, statik HTML ciktisi
├── SEO Katmani: JSON-LD semaları, dinamik site haritası, Metadata API
├── Stil Katmani: CSS custom properties ve Tailwind utility'leri
├── Analitik Katmani: Sunucu tarafli GA4, bloklama yapmayan
└── Altyapi Katmani: Vercel Edge, sifir bakim
Üç yıl sonra React Server Components'in yerini daha iyi bir şey alırsa, yalnızca Rendering Katmanı değişir. İçerik, routing, SEO ve stil katmanları bağımsız. İşte uzun vadeli mimari kazanım bu.
Ders
Portfolyo siteleri mimari olarak başarısız olur çünkü geliştiriciler sürdürülebilirlik değil, lansmanın hızını optimize eder. İroni şu ki, upfront'ta yüzde 30 daha uzun süren kararlar — dinamik site haritaları, CSS custom properties, JSON-LD şemaları, locale konvansiyonları — sonraki yıllarda bakım sürtünmesinin yüzde 90'ını ortadan kaldırıyor.
40'tan fazla blog yazısı, 3 dil ve yeni bir özellik eklemek için 5 dakikası olan 2 yıl sonraki kendiniz için inşa edin. O versiyon, ilk günde aldığınız her ekstra karar için size teşekkür edecek.
