İçeriğe geç
nextjs

Next.js Dynamic Routing: Dinamik Sayfalar Oluşturma

Tarık Tunç
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
İpucu: 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();
}
Uyarı: Catch-all segments ile optional catch-all segments arasındaki fark önemlidir. [...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>
  );
}
Bilgi: 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.

Kaynaklar

İlgili Yazılar