Next.js 16 App Router: Production Patterns and Pitfalls

Production-ready Next.js 16 App Router patterns: server components, caching strategies, metadata API, error boundaries, and performance pitfalls to avoid.

E
ECOSIRE Research and Development Team
|19 Mart 202610 dk okuma2.3k Kelime|

Next.js 16 Uygulama Yönlendiricisi: Üretim Modelleri ve Tuzaklar

Next.js Uygulama Yönlendiricisi, React uygulamalarını oluşturma şeklimizi temelden değiştirdi ve Next.js 16 ile birlikte kalıplar gerçekten üretime hazır hale geldi. Ancak Pages Router'dan kavramsal değişim oldukça diktir; sunucu bileşenleri, kısmi ön işleme, iç içe yerleşimler ve tamamen yeniden tasarlanmış bir veri getirme modelinin tümü, belgelerin tek başına tam olarak aktaramayacağı zihinsel modelin yeniden yapılandırılmasını gerektirir.

Bu kılavuz, üretimde gerçekten önemli olan modelleri kapsar: önbellek zehirlenmesini önleyen, gereksiz istemci paketlerini ortadan kaldıran ve Önemli Web Verilerinizi uygun ölçekte yeşil tutan modeller. 11 yerel ayarlı i18n, dinamik OG görüntüleri ve 5.577 MDX içerik dosyası için sunucu tarafı işlemeyle 249 sayfalık bir Next.js 16 uygulaması oluşturma deneyiminden yararlanacağız.

Önemli Çıkarımlar

  • Yerel ayarlara duyarlı SEO gerektiren sayfalarda asla export const metadata kullanmayın; her zaman generateMetadata() kullanın
  • Sunucu Bileşenleri varsayılandır; 'use client' yalnızca gerçekten etkileşime ihtiyacınız olduğunda ekleyin
  • loading.tsx ve error.tsx kökte değil rota segmentleriyle aynı yerde bulunur
  • proxy.ts dosyası Next.js 16'nın ara yazılımıdır — hem middleware.ts hem de proxy.ts oluşturmayın
  • Sunucu Bileşenlerindeki fetch(), aynı işleme içindeki aynı istekleri otomatik olarak tekilleştirir
  • unstable_cache etiketli, her şeyi yeniden doğrulamadan cerrahi önbellek geçersiz kılmayı sağlar
  • Derleme sırasında statik olarak oluşturmak istediğiniz dinamik rotalar için generateStaticParams() gereklidir
  • JSON-LD yapılandırılmış verilerinin, unicode kodlamayı kullanarak komut dosyası aralarını temizlemesi gerekir

Sunucu Bileşeni Zihinsel Modeli

Geliştiricilerin Uygulama Yönlendiricisi ile yaptığı en büyük hata, 'use client''a çok erken ulaşmaktır. Next.js 16'da app/ dizinindeki her bileşen varsayılan olarak bir Sunucu Bileşenidir. Bu doğru varsayılandır; Sunucu Bileşenleri sunucuda oluşturulur, sıfır JavaScript paketi etkisine sahiptir ve veritabanlarına ve API'lere doğrudan erişebilir.

Kural: 'use client' öğesini bileşen ağacınızın yapraklarına itin. Bir sayfa, verileri alıp etkileşimleri yöneten ince İstemci Bileşeni kabuğuna aktaran bir Sunucu Bileşeni olabilir.

// app/[locale]/blog/[slug]/page.tsx — Server Component (no directive needed)
import { getBlogPost } from '@/lib/blog';
import { BlogContent } from '@/components/blog/blog-content'; // Client Component

export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string; locale: string }>;
}) {
  const { slug, locale } = await params;
  const post = await getBlogPost(slug, locale);

  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      {/* Server Component — no JS sent to browser */}
      <PostMeta author={post.author} date={post.date} />
      {/* Client Component — only this file sent as JS */}
      <BlogContent content={post.content} />
    </article>
  );
}
// components/blog/blog-content.tsx — Client Component
'use client';

import { useState } from 'react';

export function BlogContent({ content }: { content: string }) {
  const [expanded, setExpanded] = useState(false);

  return (
    <div>
      <div>{content}</div>
      {/* Client-side interactivity lives here */}
    </div>
  );
}

Temel bilgi: BlogContent tarayıcıya JavaScript gönderir. PostMeta bunu yapmaz. Gereksiz her 'use client' yönergesi paketinizi şişirir.


Statik Meta Veri Üzerinden createdMetadata

Statik export const metadata basitliği nedeniyle caziptir, ancak yerel ayara duyarlı olamaz, rota parametrelerine erişemez ve veri getiremez. Herhangi bir gerçek uygulama için generateMetadata() tek doğru seçimdir:

// app/[locale]/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string; locale: string }>;
}): Promise<Metadata> {
  const { slug, locale } = await params;
  const post = await getBlogPost(slug, locale);

  if (!post) return {};

  const locales = ['en', 'zh', 'es', 'ar', 'pt', 'fr', 'de', 'ja', 'tr', 'hi', 'ur'];

  return {
    title: post.title,
    description: post.description,
    alternates: {
      canonical: `https://ecosire.com/${locale === 'en' ? '' : locale + '/'}blog/${slug}`,
      languages: Object.fromEntries(
        locales.map((loc) => [
          loc,
          `https://ecosire.com/${loc === 'en' ? '' : loc + '/'}blog/${slug}`,
        ])
      ),
    },
    openGraph: {
      title: post.title,
      description: post.description,
      images: [`/api/og/blog/${slug}?locale=${locale}`],
      type: 'article',
      publishedTime: post.date,
    },
  };
}

alternates.languages alanı her yerel ayar için hreflang etiketleri oluşturur; bu, çok dilli SEO için gereklidir. Arama motorları bunları her kullanıcıya doğru dil sürümünü sunmak için kullanır.


İç İçe Düzenler ve Paylaşılan Durum

Uygulama Yönlendiricinin iç içe düzen sistemi güçlüdür ancak bariz olmayan bir sınırlaması vardır: düzenler, verileri destek yoluyla alt öğelerine aktaramaz. Geçici çözüm, React Context (yalnızca İstemci Bileşenleri) veya verileri her düzeyde bağımsız olarak getirmektir.

app/
  [locale]/
    layout.tsx          <- Locale layout: fonts, i18n provider, theme
    page.tsx            <- Homepage
    blog/
      layout.tsx        <- Blog layout: sidebar, breadcrumbs
      page.tsx          <- Blog list
      [slug]/
        layout.tsx      <- Post layout: reading progress, TOC
        page.tsx        <- Post content
        loading.tsx     <- Skeleton while post loads
        error.tsx       <- Error boundary for this segment

Her layout.tsx alt öğelerini bağımsız olarak sarar. Ana düzende getirilen veriler çocukların kullanımına otomatik olarak sunulmaz; veriye ihtiyaç duyan her bileşen, verileri bağımsız olarak getirir. Next.js 16, bir oluşturma döngüsü içinde aynı fetch() çağrılarını tekilleştirir, dolayısıyla bu, göründüğü kadar pahalı değildir.

// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  const messages = await getMessages();

  return (
    <html lang={locale} dir={['ar', 'ur'].includes(locale) ? 'rtl' : 'ltr'}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Önbelleğe Alma Stratejisi: Dört Katman

Next.js 16'nın dört farklı önbellekleme katmanı vardır. Bir sayfanın neden güncellenip güncellenmediğini anlamak için dördünü de anlamak gerekir:

1. Not Alma İsteği — Bir oluşturma döngüsü içindeki aynı fetch() URL'si bir kez çağrılır. Otomatik, yapılandırma yok.

2. Veri Önbelleği — İstekler arasında kalıcıdır. fetch() yanıtları varsayılan olarak süresiz olarak önbelleğe alınır.

// Force revalidation every hour
const data = await fetch('/api/posts', { next: { revalidate: 3600 } });

// Never cache (equivalent to SSR)
const data = await fetch('/api/posts', { cache: 'no-store' });

3. Tam Rota Önbelleği — Statik rotalar için oluşturma sırasında önbelleğe alınan statik HTML ve RSC yükü.

4. Yönlendirici Önbelleği — Ziyaret edilen rotaların istemci tarafı önbelleği. Tarayıcı oturumu boyunca devam eder.

Veritabanı sorguları için (getirilmeyen), unstable_cache kullanın:

import { unstable_cache } from 'next/cache';

const getCachedBlogPosts = unstable_cache(
  async (locale: string) => {
    return db.select().from(posts).where(eq(posts.locale, locale));
  },
  ['blog-posts'], // Cache key
  {
    revalidate: 3600, // Revalidate hourly
    tags: ['blog'], // Tag for manual invalidation
  }
);

// Invalidate from a Server Action or Route Handler:
import { revalidateTag } from 'next/cache';
revalidateTag('blog'); // All blog-tagged caches cleared

Hata Sınırları: İki Düzey Gereklidir

Üretim uygulamalarının iki hata sınırına ihtiyacı vardır: biri yerel ayarlara duyarlı sayfalar için, diğeri ise kök düzeyindeki hatalar için genel geri dönüş.

// app/[locale]/error.tsx — Locale-level error boundary
'use client';

import { useTranslations } from 'next-intl';
import { useEffect } from 'react';

export default function LocaleError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  const t = useTranslations('errors');

  useEffect(() => {
    // Log to error tracking service
    console.error(error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <h2>{t('title')}</h2>
      <p>{t('description')}</p>
      <button onClick={reset}>{t('retry')}</button>
    </div>
  );
}
// app/global-error.tsx — Root fallback (no i18n available here)
'use client';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  // Cannot use useTranslations here — no NextIntlClientProvider above
  // Use inline styles — no Tailwind in root error boundaries
  return (
    <html>
      <body>
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '2rem' }}>
          <h2 style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>Something went wrong</h2>
          <button
            onClick={reset}
            style={{ marginTop: '1rem', padding: '0.5rem 1rem', cursor: 'pointer' }}
          >
            Try again
          </button>
        </div>
      </body>
    </html>
  );
}

global-error.tsx tetiklendiğinde layout.tsx kökünün yerini alır, dolayısıyla <html> ve <body> etiketlerini içermelidir. Düzende yaşayan içerik sağlayıcılara erişemez; i18n yok, tema yok, kimlik doğrulama bağlamı yok.


Ara yazılım: proxy.ts ve ara katman yazılımı.ts

Next.js, uç ara katman yazılımı için middleware.ts (veya middleware.js) kullanır. next-intl v4 kullanan bir projede, ara katman yazılımı kurulumu sıklıkla proxy.ts olarak yeniden adlandırılır ve yeniden dışa aktarılır, bu da kritik bir tuzak oluşturur: hem middleware.ts hem de proxy.ts oluşturmayın. Her ikisine de sahip olmak derleme hatasına neden olur.

İ18n'yi kimlik doğrulama korumasıyla birleştirmek için doğru model:

// src/proxy.ts — This IS the Next.js middleware
import createMiddleware from 'next-intl/middleware';
import { NextRequest, NextResponse } from 'next/server';
import { routing } from './i18n/routing';

const intlMiddleware = createMiddleware(routing);

const protectedPaths = ['/dashboard', '/portal'];

export default function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Check auth for protected paths
  const isProtected = protectedPaths.some((path) =>
    pathname.includes(path)
  );

  if (isProtected) {
    const token = request.cookies.get('ecosire_auth');
    if (!token) {
      const url = request.nextUrl.clone();
      url.pathname = '/auth/login';
      // Prevent open redirect — never allow // prefix
      const redirect = pathname.startsWith('//') ? '/' : pathname;
      url.searchParams.set('redirect', redirect);
      return NextResponse.redirect(url);
    }
  }

  return intlMiddleware(request);
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)'],
};

Bu tek dosya hem i18n yerel ayar tespitini hem de kimlik doğrulama korumasını yönetir. config.matcher statik dosyaları, API rotalarını ve Next.js dahili öğelerini hariç tutar.


JSON-LD Yapılandırılmış Veri Güvenliği

JSON-LD komut dosyaları yaygın bir XSS vektörüdür. Verileriniz bir kapanış komut dosyası etiketi içeriyorsa komut dosyası bloğunun dışına çıkabilir. Düzeltme zorunludur; küçük karakteri kodlayın:

// components/seo/JsonLd.tsx
interface JsonLdProps {
  data: Record<string, unknown>;
}

export function JsonLd({ data }: JsonLdProps) {
  // Sanitize script tag breakout attempts by encoding '<' as unicode
  const json = JSON.stringify(data).replace(/</g, '\\u003c');

  return (
    <script
      type="application/ld+json"
      // Safe: content is JSON-serialized and sanitized above
      suppressHydrationWarning
      ref={(el) => { if (el) el.textContent = json; }}
    />
  );
}

Sayfada kullanım:

<JsonLd
  data={{
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: post.title,
    description: post.description,
    inLanguage: locale,
    author: {
      '@type': 'Organization',
      name: 'ECOSIRE',
    },
  }}
/>

inLanguage alanı çok dilli SEO için gereklidir; AI tarayıcılarına ve arama motorlarına içeriğin hangi dilde olduğunu söyler ve İngilizce olmayan sorgular için sıralama doğruluğunu artırır.


Dinamik OG Görselleri

Next.js 16'nın ImageResponse API'si uçta sayfa başına OG görüntüleri oluşturur. Blog gönderileri için kurulum:

// app/api/og/blog/[slug]/route.tsx
import { ImageResponse } from 'next/og';

export const runtime = 'edge';

export async function GET(
  request: Request,
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params;
  const post = await getPostMeta(slug);

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'flex-end',
          width: '100%',
          height: '100%',
          background: 'linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%)',
          padding: '60px',
        }}
      >
        <div style={{ fontSize: 48, fontWeight: 700, color: 'white', lineHeight: 1.2 }}>
          {post.title}
        </div>
        <div style={{ fontSize: 24, color: '#94a3b8', marginTop: 20 }}>
          {post.description}
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

Yaygın Tuzaklar ve Çözümler

Tuzak 1: Sunucu Bileşenleri çalışacakken İstemci Bileşenlerindeki verileri almak

Bir bileşen yalnızca verileri görüntülüyorsa ve React durumuna veya tarayıcı API'lerine ihtiyaç duymuyorsa, bunun bir Sunucu Bileşeni olması gerekir. 'use client' eklenmesi, içe aktardığı her şeyi istemci paketine zorlar.

2. Tuzak: Eşzamansız Sunucu Bileşenleri etrafında Eksik Askıya Alma sınırları

// Without Suspense — entire page blocks until data loads
export default async function Page() {
  const data = await slowFetch(); // 2 seconds
  return <div>{data}</div>;
}

// With Suspense — page streams, slow part shows skeleton
export default function Page() {
  return (
    <Suspense fallback={<DataSkeleton />}>
      <SlowDataComponent />
    </Suspense>
  );
}

3. Tuzak: Önbelleğe alınmış işlevlerde çerezleri() veya başlıkları() kullanmak

cookies() ve headers(), rotayı dinamik oluşturmaya dahil eden dinamik API'lerdir. Bunları bir unstable_cache geri çağırma içinde kullanmak, önbelleği sessizce bozar.

4. Tuzak: Next.js 16'da parametreleri beklemeyi unutmak

Next.js 16'da params artık bir Promise'dir. await öğesinin unutulması çalışma zamanı hatalarına neden olur:

// Next.js 15 — params was synchronous
export default function Page({ params }: { params: { slug: string } }) {
  const { slug } = params;
}

// Next.js 16 — params is async
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params; // Required
}

Sıkça Sorulan Sorular

Ne zaman dinamik oluşturma yerine createdStaticParams() kullanmalıyım?

Sık sık değişen içerik için (blog gönderileri, ürün sayfaları, belgeler) generateStaticParams() kullanın. Bu, sıfır çalışma zamanı maliyetiyle derleme sırasında statik HTML oluşturur. Kişiselleştirilmiş içerik, gerçek zamanlı veriler veya oluşturma süresinin engelleyici olacağı binlerce değişken içeren sayfalar için dinamik oluşturmayı kullanın. Ara sıra değişen içerik için her ikisini de ISR (revalidate) ile birleştirin.

Uygulama Yönlendiricisinde kimlik doğrulamayı nasıl hallederim?

Kimlik doğrulama belirteçlerini HttpOnly çerezlerinde saklayın; asla localStorage veya sessionStorage. Korunan rotalara ilişkin çerez varlığını kontrol etmek için ara yazılım (proxy.ts) kullanın. Sunucu Bileşenleri'nde, next/headers'den cookies() içeren çerezleri okuyun. Kullanıcı verilerini sunucudan istemciye props veya React Context aracılığıyla aktarın; asla İstemci Bileşenlerindeki çerezleri yeniden okuyarak.

loading.tsx ile Suspense arasındaki fark nedir?

loading.tsx, Next.js'nin tüm rota segmenti için otomatik olarak bir Askıya Alma sınırına sardığı özel bir dosyadır. Segmentin eşzamansız içeriği yüklenirken hemen gösterilir. Manuel Suspense sınırları size daha hassas kontrol sağlar; bir sayfanın farklı bölümleri için farklı çerçeveler gösterebilirsiniz. Her ikisi de aynı temel React Suspense mekanizmasını kullanır.

Sunucu ve İstemci Bileşenleri arasında durumu nasıl paylaşırım?

Yapamazsınız — Sunucu Bileşenleri bir kez oluşturulur ve çalışma zamanı durumu yoktur. Verileri sunucudan istemciye sahne donanımı aracılığıyla aktarın. Gezinmelerde devam etmesi gereken istemci tarafı durumu için URL arama parametrelerini (paylaşılabilir durum için), Zustand veya Redux'u (geçici durum için) veya çerezleri (kullanıcı tercihleri ​​için) kullanın. Hiçbir zaman bir İstemci Bileşeninin durumunu bir Sunucu Bileşenine aktarmaya çalışmayın.

Geliştirme için Turbopack'i nasıl kurarım?

Geliştirici betiğinize --turbo ekleyin: "dev": "next dev --turbo". Turbopack, Next.js 16'nın Rust tabanlı paketleyicisidir ve çok daha hızlı soğuk başlatma ve HMR'ye sahiptir. Çoğu proje için üretime hazırdır ancak bazı web paketi eklenti uyumsuzlukları vardır. Geçiş yapmadan önce Turbopack uyumluluk listesine göre özel bağımlılıklarınızı kontrol edin.


Sonraki Adımlar

11 yerel ayarı, binlerce sayfayı ve kurumsal düzeyde performans gereksinimlerini karşılayan bir üretim Next.js 16 uygulaması oluşturmak önemli bir mühendislik girişimidir. ECOSIRE'ın ön uç ekibi tam olarak bunu sundu: çok dilli SEO, dinamik OG görselleri ve saniyeden kısa yanıt süreleri sunan 249 sayfalık Next.js 16 platformu.

Next.js uygulamanızı ölçeklendiriyorsanız veya kurumsal ön uç mühendislik desteğine ihtiyacınız varsa nasıl yardımcı olabileceğimizi görmek için geliştirme hizmetlerimizi keşfedin.

E

Yazan

ECOSIRE Research and Development Team

ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.

WhatsApp'ta Sohbet Et