Next.js Dynamic Routing: Dinamik Sayfalar Oluşturma

Next.js Dynamic Routing: Dinamik Sayfalar Oluşturma
Modern web uygulamalarında sayfalar genellikle dinamik içeriklere sahiptir. Bir blog yazısı, bir ürün detayı veya bir kullanıcı profili gibi sayfalar, URL parametrelerine göre farklı içerikler gösterir. Next.js'in Dynamic Routing sistemi, köşeli parantezlerle dinamik URL segmentleri tanımlayarak bu tür sayfaları kolayca oluşturmanızı sağlar.
Temel Dynamic Routes
Dinamik bir segment oluşturmak için klasör adını köşeli parantezlerle sararsınız:
// Dosya yapısı
src/app/
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/nextjs-rehberi, /blog/react-hooks, vb.
├── products/
│ └── [id]/
│ └── page.tsx # /products/1, /products/42, vb.
└── users/
└── [username]/
└── page.tsx # /users/tariktunc, /users/john, vb.
Dinamik Parametre Okuma
Dinamik segment değeri, params prop'u üzerinden okunur:
// app/blog/[slug]/page.tsx
interface BlogPostProps {
params: { slug: string };
}
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 },
});
if (!res.ok) return null;
return res.json();
}
export default async function BlogPost({ params }: BlogPostProps) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<article className="max-w-3xl mx-auto py-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-gray-600 mb-8">
<span>{post.author}</span>
<time>{new Date(post.date).toLocaleDateString('tr-TR')}</time>
<span>{post.readingTime} dk okuma</span>
</div>
<div className="prose prose-lg" dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
generateStaticParams ile Statik Oluşturma
Dinamik route'ları build zamanında statik olarak oluşturmak için generateStaticParams kullanın:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((r) =>
r.json()
);
return posts.map((post: Post) => ({
slug: post.slug,
}));
// Örnek çıktı: [{ slug: 'nextjs-rehberi' }, { slug: 'react-hooks' }]
}
// dynamicParams kontrolü
export const dynamicParams = true; // true: tanımsız slug'lar on-demand render
// false: tanımsız slug'lar 404 döner
generateStaticParams, parent-child ilişkisinde de çalışır. Üst segmentin parametreleri alt segmente aktarılır, böylece veritabanı sorgularını optimize edebilirsiniz.
Çoklu Dinamik Segmentler
Bir route'ta birden fazla dinamik segment olabilir:
// app/shop/[category]/[productId]/page.tsx
interface ProductPageProps {
params: {
category: string;
productId: string;
};
}
export default async function ProductPage({ params }: ProductPageProps) {
const { category, productId } = params;
const product = await getProduct(category, productId);
return (
<div>
<nav className="text-sm text-gray-500 mb-4">
Ana Sayfa > {category} > {product.name}
</nav>
<h1>{product.name}</h1>
<p>Kategori: {category}</p>
<p>Fiyat: {product.price} TL</p>
</div>
);
}
export async function generateStaticParams() {
const products = await getProducts();
return products.map((p: Product) => ({
category: p.category,
productId: p.id.toString(),
}));
}
Catch-All Segments
Üç nokta (...) ile birden fazla alt segmenti yakalayabilirsiniz:
// app/docs/[...slug]/page.tsx
// /docs/getting-started → slug: ['getting-started']
// /docs/api/reference → slug: ['api', 'reference']
// /docs/api/v2/endpoints → slug: ['api', 'v2', 'endpoints']
interface DocsPageProps {
params: { slug: string[] };
}
export default async function DocsPage({ params }: DocsPageProps) {
const { slug } = params;
const path = slug.join('/');
const doc = await getDoc(path);
return (
<div className="flex">
<aside className="w-64">
<DocsSidebar currentPath={path} />
</aside>
<main className="flex-1 p-8">
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.content }} />
</main>
</div>
);
}
Optional Catch-All Segments
Çift köşeli parantez ile optional catch-all oluşturabilirsiniz. Bu, root path'i de yakalar:
// app/shop/[[...slug]]/page.tsx
// /shop → slug: undefined
// /shop/shoes → slug: ['shoes']
// /shop/shoes/nike → slug: ['shoes', 'nike']
interface ShopPageProps {
params: { slug?: string[] };
}
export default async function ShopPage({ params }: ShopPageProps) {
const { slug } = params;
if (!slug) {
// /shop — tüm ürünleri göster
const products = await getAllProducts();
return <ProductGrid products={products} />;
}
if (slug.length === 1) {
// /shop/shoes — kategori sayfası
const products = await getProductsByCategory(slug[0]);
return <ProductGrid products={products} category={slug[0]} />;
}
if (slug.length === 2) {
// /shop/shoes/nike — marka filtreli
const products = await getProductsByBrand(slug[0], slug[1]);
return <ProductGrid products={products} />;
}
notFound();
}
[...slug] root path'de (/shop) 404 dönerken, [[...slug]] root path'i de yakalar.
Dynamic Metadata Oluşturma
Dinamik sayfalarda SEO için dinamik metadata oluşturabilirsiniz:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await getPost(params.slug);
if (!post) {
return { title: 'Yazı Bulunamadı' };
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage }],
type: 'article',
publishedTime: post.date,
authors: [post.author],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
},
};
}
Search Params ile Çalışma
URL query parametrelerine searchParams prop'u ile erişebilirsiniz:
// app/search/page.tsx
// URL: /search?q=nextjs&page=2&sort=date
interface SearchPageProps {
searchParams: {
q?: string;
page?: string;
sort?: string;
};
}
export default async function SearchPage({ searchParams }: SearchPageProps) {
const query = searchParams.q || '';
const page = parseInt(searchParams.page || '1');
const sort = searchParams.sort || 'relevance';
const results = await searchPosts({ query, page, sort });
return (
<div>
<h1>Arama Sonuçları: "{query}"</h1>
<p>{results.total} sonuç bulundu</p>
{results.items.map((item) => (
<SearchResult key={item.id} item={item} />
))}
<Pagination current={page} total={results.totalPages} />
</div>
);
}
searchParams kullanan sayfalar otomatik olarak dinamik (SSR) olur. Build zamanında statik oluşturma yapılamaz çünkü query parametreleri istek zamanında belirlenir.
Sonuç
Next.js Dynamic Routing, dosya sistemi tabanlı yaklaşımıyla dinamik sayfa oluşturmayı son derece sezgisel hale getirir. Tek segment, çoklu segment, catch-all ve optional catch-all gibi farklı pattern'ler ile her türlü URL yapısını karşılayabilirsiniz. generateStaticParams ile performansı artırırken, generateMetadata ile SEO'yu güçlendirebilirsiniz.