ViraStack
  • Academy
  • About
  • Support
HomeSourcesContribute
ViraStack

A frontend ecosystem: free guides, project templates, AI rules, and form components.

Products

  • ViraStack Start
  • ViraStack AI
  • ViraStack Mask
  • ViraStack Password
  • ViraStack Guide

Explore

  • Academy
  • About
  • Brand
  • Support

© 2026 ViraStack.

Open source · MIT

Next.js

12 / 12Project

Ürün kataloğu

Cache, streaming, Server Action ve metadata ile ürün listesi, detay sayfası ve sepet.

Senaryo

Küçük bir mağaza kuracaksınız. Uygulama:

  • /products adresinde ürünleri listeler; arama kutusu aramayı URL'ye yazar.

  • Ürün sayfasında adı ve fiyatı cache'ten anında, stoğu istek anında akıtarak gösterir.

  • Sepete ekle stoğu sunucuda kontrol eder, sepeti cookie'ye yazar; üstteki sepet sayısı aynı yanıtta güncellenir.

  • /cart sepeti listeler ve ürün kaldırır.

  • Bir CMS ürün değişince webhook'u çağırır, ilgili cache yenilenir.

  • Her ürünün başlığı, canonical adresi, paylaşım görseli ve JSON-LD'si olur; site haritası ve robots.txt üretilir.

  • Bir instant() testi ürün sayfasına geçişin anında kaldığını korur.

Amaç tasarım değil, derslerdeki parçaların birlikte çalışmasıdır. Her parçanın hangi dersten geldiği:

ParçaDosyaDers
"use cache", cacheLife, cacheTag, connection()lib/products.tsCache ve render modeli
Cookie'de sepet, server-onlylib/cart.tsServer ve Client Components
Kök layout, <Suspense> içinde sepet linkiapp/layout.tsx, components/cart-link.tsxRouting ve layout, Veri çekme ve streaming
next/form, searchParamsapp/products/page.tsx, components/product-list.tsxRouting ve layout
params'ı <Suspense> içinde okumak, prefetchapp/products/[slug]/page.tsxNavigasyon ve prefetch
Server Action, useActionState, bindapp/actions.ts, components/add-to-cart-form.tsx, app/cart/page.tsxServer Actions
notFound, error.tsx ve retrynot-found.tsx, error.tsxHata yönetimi
Webhook ile revalidateTagapp/api/revalidate/route.tsRoute Handler ve proxy
generateMetadata, OG görseli, sitemap, JSON-LDapp/products/[slug]/, app/sitemap.ts, app/robots.tsMetadata ve SEO
instant() testie2e/product.spec.tsNavigasyon ve prefetch

Adım 1: Projeyi Kurun

Kurulum ve proje yapısı dersindeki gibi varsayılanlarla bir proje açın:1

$ pnpm create next-app@latest my-shop --yes

next.config.ts'te cacheComponents: true ve partialPrefetching: true hazır gelir; bu proje ikisine de dayanıyor. app/page.tsx'in içeriğini Adım 4'te değiştireceksiniz.

Adım 2: Veri Katmanı

Gerçek bir veritabanı yerine gecikmeli fonksiyonlar kullanın. Ürün bilgisi saatlerce değişmez, cache'lenir. Stok ise her istekte güncel olmalı.

// lib/products.ts
import "server-only";

import { cacheLife, cacheTag } from "next/cache";
import { connection } from "next/server";

export type Product = {
  slug: string;
  name: string;
  price: number;
  description: string;
};

const products: Product[] = [
  { slug: "canvas-sneaker", name: "Kanvas Ayakkabı", price: 1499, description: "Günlük kullanım için hafif kanvas ayakkabı." },
  { slug: "leather-backpack", name: "Deri Sırt Çantası", price: 2899, description: "Laptop bölmeli, el yapımı deri çanta." },
  { slug: "wool-beanie", name: "Yün Bere", price: 449, description: "Yumuşak merinos yünden bere." },
];

const stock: Record<string, number> = {
  "canvas-sneaker": 12,
  "leather-backpack": 0,
  "wool-beanie": 4,
};

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export async function getProducts(query = "") {
  "use cache";
  cacheLife("hours");
  cacheTag("products");

  await wait(300);
  const q = query.toLocaleLowerCase("tr");
  return products.filter((product) => product.name.toLocaleLowerCase("tr").includes(q));
}

export async function getProduct(slug: string) {
  "use cache";
  cacheLife("hours");
  cacheTag("products", `product-${slug}`);

  await wait(300);
  return products.find((product) => product.slug === slug) ?? null;
}

export async function getStock(slug: string) {
  await connection();
  await wait(800);
  return stock[slug] ?? 0;
}
  • "use cache" sonucu argümanlara göre saklar. getProduct("canvas-sneaker") ile getProduct("wool-beanie") ayrı kayıtlardır.2

  • İki etiket var: products tüm listeyi, product-${slug} tek ürünü yeniler. Adım 10'daki webhook bunları kullanacak.3

  • connection() Next.js'e "bunu prerender etme, istek anında çalıştır" der. Stok her ziyarette yeniden okunur.2

Adım 3: Cookie'de Sepet

Sepeti bir cookie'de JSON olarak tutun. Cookie okumak Server Component'te serbesttir; yazmak ise yalnızca bir Server Action ya da Route Handler'da olur, çünkü cookie yanıt başlığıyla gönderilir.4

// lib/cart.ts
import "server-only";

import { cookies } from "next/headers";

export type Cart = Record<string, number>;

const CART_COOKIE = "cart";

export async function getCart(): Promise<Cart> {
  const value = (await cookies()).get(CART_COOKIE)?.value;
  if (!value) return {};

  try {
    return JSON.parse(value) as Cart;
  } catch {
    return {};
  }
}

export async function saveCart(cart: Cart) {
  (await cookies()).set(CART_COOKIE, JSON.stringify(cart), {
    httpOnly: true,
    sameSite: "lax",
    path: "/",
  });
}

httpOnly cookie'yi tarayıcıdaki JavaScript'ten gizler. Yine de kullanıcı onu DevTools'tan değiştirebilir; bu yüzden içindeki her değeri güvenilmez kabul edeceksiniz (Adım 7 ve 8).

Adım 4: Layout ve Sepet Linki

Sepet sayısı cookie'den gelir, yani istek anında hesaplanır. Onu <Suspense> içine alırsanız header'ın geri kalanı statik kabukta kalır.2

// components/cart-link.tsx
import Link from "next/link";

import { getCart } from "@/lib/cart";

export async function CartLink() {
  const cart = await getCart();
  const count = Object.values(cart).reduce((sum, quantity) => sum + quantity, 0);

  return <Link href="/cart">Sepet ({count})</Link>;
}
// app/layout.tsx
import type { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";

import { CartLink } from "@/components/cart-link";

import "./globals.css";

export const metadata: Metadata = {
  metadataBase: new URL("https://shop.example.com"),
  title: { default: "Mağaza", template: "%s | Mağaza" },
  description: "Next.js ile kurulmuş küçük bir ürün kataloğu.",
};

export default function RootLayout({ children }: LayoutProps<"/">) {
  return (
    <html lang="tr">
      <body>
        <header>
          <nav>
            <Link href="/">Mağaza</Link>
            <Link href="/products">Ürünler</Link>
            <Suspense fallback={<span>Sepet</span>}>
              <CartLink />
            </Suspense>
          </nav>
        </header>
        <main>{children}</main>
      </body>
    </html>
  );
}
// app/page.tsx
import Link from "next/link";

export default function HomePage() {
  return (
    <section>
      <h1>Yeni sezon</h1>
      <p>Ayakkabı, çanta ve aksesuarlar.</p>
      <Link href="/products">Ürünleri gör</Link>
    </section>
  );
}

create-next-app'in yazdığı layout.tsx Geist font'larını next/font ile yükler. Bu dosyayla değiştirince globals.css'teki --font-geist-sans ve --font-geist-mono değişkenleri boşta kalır; onları system-ui, sans-serif ve ui-monospace, monospace ile değiştirin.

Adım 5: Ürün Listesi ve Arama

next/form, action bir adres olduğunda aramayı ?q= olarak URL'ye yazar ve istemci tarafında geçiş yapar.5 searchParams istek anında bilinir; okuyan kısım <Suspense> içinde olmalı.2

// app/products/page.tsx
import type { Metadata } from "next";
import Form from "next/form";
import { Suspense } from "react";

import { ProductList } from "@/components/product-list";

export const metadata: Metadata = { title: "Ürünler" };

export default function ProductsPage({ searchParams }: PageProps<"/products">) {
  return (
    <>
      <h1>Ürünler</h1>
      <Form action="/products">
        <input name="q" aria-label="Ürün ara" placeholder="Ara..." />
        <button>Ara</button>
      </Form>
      <Suspense fallback={<p>Ürünler yükleniyor...</p>}>
        <ProductList searchParams={searchParams} />
      </Suspense>
    </>
  );
}
// components/product-list.tsx
import Link from "next/link";

import { getProducts } from "@/lib/products";

export async function ProductList({
  searchParams,
}: Pick<PageProps<"/products">, "searchParams">) {
  const { q } = await searchParams;
  const products = await getProducts(typeof q === "string" ? q : "");

  if (products.length === 0) return <p>Sonuç yok.</p>;

  return (
    <ul>
      {products.map((product) => (
        <li key={product.slug}>
          <Link href={`/products/${product.slug}`} prefetch>
            {product.name}
          </Link>{" "}
          {product.price} TL
        </li>
      ))}
    </ul>
  );
}

q bir dizi de olabilir (?q=a&q=b); typeof kontrolü bu yüzden var. prefetch ürün sayfasındaki cache'li içeriği tıklamadan önce getirir; etkisini Adım 12'deki test gösterecek.6

Adım 6: Ürün Sayfası

Sayfanın üç katmanı var:

KatmanİçerikNe zaman gelir
App Shell<article> ve "Ürün yükleniyor..."Tüm ürün linkleri için ortak, hemen
Cache'li detayAd, açıklama, fiyat, JSON-LDprefetch ile tıklamadan önce
İstek anıStok ve Sepete ekleTıkladıktan sonra akarak
// app/products/[slug]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { Suspense } from "react";

import { AddToCartForm } from "@/components/add-to-cart-form";
import { getProduct, getProducts, getStock } from "@/lib/products";

export async function generateStaticParams() {
  const products = await getProducts();
  return products.map((product) => ({ slug: product.slug }));
}

export async function generateMetadata({
  params,
}: PageProps<"/products/[slug]">): Promise<Metadata> {
  const { slug } = await params;
  const product = await getProduct(slug);
  if (!product) return {};

  return {
    title: product.name,
    description: product.description,
    alternates: { canonical: `/products/${product.slug}` },
  };
}

export default function ProductPage({ params }: PageProps<"/products/[slug]">) {
  return (
    <article>
      <Suspense fallback={<p>Ürün yükleniyor...</p>}>
        <ProductDetails params={params} />
      </Suspense>
    </article>
  );
}

async function ProductDetails({ params }: Pick<PageProps<"/products/[slug]">, "params">) {
  const { slug } = await params;
  const product = await getProduct(slug);
  if (!product) notFound();

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "Product",
    name: product.name,
    description: product.description,
    offers: { "@type": "Offer", price: product.price, priceCurrency: "TRY" },
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c") }}
      />
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>{product.price} TL</p>
      <Suspense fallback={<p>Stok kontrol ediliyor...</p>}>
        <Stock slug={product.slug} />
      </Suspense>
    </>
  );
}

async function Stock({ slug }: { slug: string }) {
  const count = await getStock(slug);
  if (count === 0) return <p>Tükendi</p>;

  return (
    <>
      <p>Stokta {count} adet</p>
      <AddToCartForm slug={slug} />
    </>
  );
}
  • ProductPage async değil ve params'ı await etmiyor. Promise'i <Suspense> içindeki child'a veriyor. await params sayfanın en üstünde olsaydı geliştirmede "URL data outside of Suspense" uyarısı çıkardı.6

  • generateStaticParams üç ürünü build'de prerender eder. Ürüne doğrudan gelen ziyaretçi adı statik HTML'de görür.7

  • generateMetadata ve ProductDetails aynı getProduct'ı çağırıyor; "use cache" sayesinde veri bir kez okunur.8

Adım 7: Sepete Ekle

Server Action stoğu ve ürünü sunucuda doğrular. İstemciden gelen slug'a güvenmez.9

// app/actions.ts
"use server";

import { getCart, saveCart } from "@/lib/cart";
import { getProduct, getStock } from "@/lib/products";

export type AddToCartState = { message: string | null };

export async function addToCart(
  _prev: AddToCartState,
  formData: FormData,
): Promise<AddToCartState> {
  const slug = String(formData.get("slug"));
  const product = await getProduct(slug);
  if (!product) return { message: "Ürün bulunamadı." };

  const cart = await getCart();
  const quantity = (cart[slug] ?? 0) + 1;
  if (quantity > (await getStock(slug))) return { message: "Stokta yeterli ürün yok." };

  await saveCart({ ...cart, [slug]: quantity });
  return { message: `${product.name} sepete eklendi.` };
}

export async function removeFromCart(slug: string) {
  const cart = await getCart();
  delete cart[slug];
  await saveCart(cart);
}
// components/add-to-cart-form.tsx
"use client";

import { useActionState } from "react";

import { addToCart } from "@/app/actions";

export function AddToCartForm({ slug }: { slug: string }) {
  const [state, formAction, isPending] = useActionState(addToCart, { message: null });

  return (
    <form action={formAction}>
      <input type="hidden" name="slug" value={slug} />
      <button disabled={isPending}>{isPending ? "Ekleniyor..." : "Sepete ekle"}</button>
      {state.message && <p role="status">{state.message}</p>}
    </form>
  );
}

Header'daki Sepet (1) için hiçbir şey yazmadınız. Bir Server Action cookie yazınca Next.js o sayfayı sunucuda yeniden render eder ve yeni arayüzü action'ın yanıtıyla aynı turda gönderir.49 Stok kontrolü bir hata değil, beklenen bir durum; bu yüzden throw yerine mesaj dönüyor.

Adım 8: Sepet Sayfası

// app/cart/page.tsx
import type { Metadata } from "next";
import { Suspense } from "react";

import { removeFromCart } from "@/app/actions";
import { getCart } from "@/lib/cart";
import { getProduct } from "@/lib/products";

export const metadata: Metadata = { title: "Sepet", robots: { index: false } };

export default function CartPage() {
  return (
    <>
      <h1>Sepet</h1>
      <Suspense fallback={<p>Sepet yükleniyor...</p>}>
        <CartItems />
      </Suspense>
    </>
  );
}

async function CartItems() {
  const cart = await getCart();
  const lines = (
    await Promise.all(
      Object.entries(cart).map(async ([slug, quantity]) => {
        const product = await getProduct(slug);
        return product ? [{ product, quantity }] : [];
      }),
    )
  ).flat();

  if (lines.length === 0) return <p>Sepetiniz boş.</p>;

  return (
    <ul>
      {lines.map(({ product, quantity }) => (
        <li key={product.slug}>
          {product.name} x {quantity}
          <form action={removeFromCart.bind(null, product.slug)}>
            <button>Kaldır</button>
          </form>
        </li>
      ))}
    </ul>
  );
}
  • Ürünler Promise.all ile paralel okunur; sırayla await etmek bir şelale yaratırdı.

  • Cookie'de artık var olmayan bir ürün olabilir; [] dönen satır listeden düşer.

  • bind, slug'ı action'a ilk argüman olarak bağlar. Form JavaScript yüklenmeden de çalışır.10

  • robots: { index: false } sepet sayfasını arama sonuçlarından çıkarır.

Adım 9: Bulunamadı ve Hata

// app/products/[slug]/not-found.tsx
import Link from "next/link";

export default function ProductNotFound() {
  return (
    <>
      <h1>Ürün bulunamadı</h1>
      <Link href="/products">Tüm ürünler</Link>
    </>
  );
}
// app/products/error.tsx
"use client";

export default function ProductsError({ retry }: { error: unknown; retry: () => void }) {
  return (
    <>
      <h2>Ürünler yüklenemedi.</h2>
      <button onClick={() => retry()}>Tekrar dene</button>
    </>
  );
}

error.tsx app/products/ altında olduğu için hem listeyi hem ürün sayfalarını korur; header yerinde kalır. /products/nope adresi not-found.tsx'i gösterir. notFound() akış başladıktan sonra çağrıldığı için durum kodu 200 kalır ve sayfaya noindex eklenir.11

Adım 10: CMS Webhook'u

// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import type { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  if (request.headers.get("x-revalidate-secret") !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { slug } = (await request.json()) as { slug?: string };
  revalidateTag(slug ? `product-${slug}` : "products", "max");

  return Response.json({ revalidated: true });
}

Gizli anahtarı .env.local'a yazın:

# .env.local
REVALIDATE_SECRET=change-me

"max" profili stale-while-revalidate çalışır: webhook'tan sonraki ilk ziyaretçi eski sayfayı hemen alır ve arka planda yenileme başlar, sonraki ziyaretçi yeni veriyi görür.3 Bu rehber yazılırken production build'de denendi: webhook'tan sonraki ilk istek eski veriyi, ikincisi yenisini gösterdi.

Adım 11: SEO Dosyaları

Metadata ve SEO dersindeki üç dosyayı ekleyin:

// app/sitemap.ts
import type { MetadataRoute } from "next";

import { getProducts } from "@/lib/products";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const products = await getProducts();

  return [
    { url: "https://shop.example.com/products" },
    ...products.map((product) => ({
      url: `https://shop.example.com/products/${product.slug}`,
    })),
  ];
}
// app/robots.ts
import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: "*", allow: "/", disallow: "/cart" },
    sitemap: "https://shop.example.com/sitemap.xml",
  };
}
// app/products/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";

import { getProduct } from "@/lib/products";

export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const product = await getProduct(slug);

  return new ImageResponse(
    <div style={{ display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", fontSize: 96, background: "white" }}>
      {product?.name ?? "Mağaza"}
    </div>,
    size,
  );
}

Next.js opengraph-image.tsx'i bulunca ürün sayfasının <head>'ine og:image etiketini kendisi ekler; metadataBase sayesinde adres tam URL olur.12

Adım 12: Anında Navigasyonu Test Edin

Playwright ve Next.js'in test yardımcısını kurun:1314

$ pnpm add -D @playwright/test @next/playwright
$ pnpm dlx playwright install chromium
// playwright.config.ts
import { defineConfig } from "@playwright/test";

export default defineConfig({
  testDir: "e2e",
  use: { baseURL: "http://localhost:3000" },
  webServer: {
    command: "npm run dev",
    url: "http://localhost:3000",
    reuseExistingServer: true,
  },
});
// e2e/product.spec.ts
import { instant } from "@next/playwright";
import { expect, test } from "@playwright/test";

test("ürün sayfası anında açılır, stok sonra gelir", async ({ page }) => {
  await page.goto("/products");

  await instant(page, async () => {
    await page.getByRole("link", { name: "Kanvas Ayakkabı" }).click();
    await page.waitForURL((url) => url.pathname === "/products/canvas-sneaker");

    await expect(page.getByRole("heading", { name: "Kanvas Ayakkabı" })).toBeVisible();
    await expect(page.getByText("Stok kontrol ediliyor...")).toBeVisible();
  });

  await expect(page.getByText("Stokta 12 adet")).toBeVisible();
});

test("sepete ekleme sepet sayısını günceller", async ({ page }) => {
  await page.goto("/products/canvas-sneaker");
  await page.getByRole("button", { name: "Sepete ekle" }).click();

  await expect(page.getByRole("status")).toHaveText("Kanvas Ayakkabı sepete eklendi.");
  await expect(page.getByRole("link", { name: "Sepet (1)" })).toBeVisible();
});
$ pnpm dlx playwright test

instant() bloğunun içinde yalnızca tıklama anında hazır olan arayüz görünür; istek anı verisi blok bitene kadar bekletilir. Test, ürün adının orada olduğunu ve stoğun henüz gelmediğini doğruluyor.14

  • waitForURL olmadan seçici, hedef sayfa gelmeden kaynak sayfadaki bir öğeyi bulabilir. Hedef URL'yi beklemek bunu önler.14

  • ProductList'teki prefetch'i silip testi yeniden çalıştırın. Ürün adı artık bloğun içinde görünmediği için test başarısız olur. Biri ileride bu linki değiştirirse CI aynı şekilde yakalar.

Testler next dev'e karşı çalışır; test API'si orada kendiliğinden açıktır.14 Playwright'ın kendisi Playwright rehberinde.

Adım 13: Son Kontrol

npm run lint
npm run build

next build tipleri de kontrol eder. Build çıktısında ürün sayfaları ◐ ile işaretlenir: statik kabuk build'de üretildi, stok istek anında akıyor. Ürün bilgisinin yenilenme süresi cacheLife("hours")'tan gelir.2

├   /products/[slug]
│ ├ ◐ /products/canvas-sneaker                1h      1d
│ ├ ◐ /products/leather-backpack              1h      1d
│ └ ◐ /products/wool-beanie                   1h      1d
├ ƒ /api/revalidate

npm start ile açın ve şu sırayı deneyin: arama, ürün sayfası, Sepete ekle, stoğu 0 olan Deri Sırt Çantası, sepetten kaldırma, /products/nope, /sitemap.xml ve /products/canvas-sneaker/opengraph-image.

Tip

Bu projeyi bir agent'la geliştirecekseniz next-dev-loop skill'ini kurun ve her değişiklikten sonra npx playwright test'i çalıştırmasını isteyin. Ayrıntısı AI agent'larla Next.js dersinde.

Kendin Dene

  • DevTools'tan cart cookie'sini {"canvas-sneaker":99} yapın. Sepet ne gösteriyor? Sepete ekle'ye basınca ne oluyor? Gerçek bir mağazada sepetin neden veritabanında tutulduğunu düşünün.

  • getProduct'tan "use cache", cacheLife ve cacheTag satırlarını silip instant() testini çalıştırın. Neden başarısız oldu? (Yalnızca "use cache"'i silerseniz cacheLife hata verir.)

  • getProduct'ın döndürdüğü açıklamaya new Date().toISOString() koyun. npm run build && npm start ile açıp webhook'u curl -X POST -H "x-revalidate-secret: change-me" -H "content-type: application/json" -d '{"slug":"canvas-sneaker"}' localhost:3000/api/revalidate ile çağırın; sayfayı iki kez yenileyin.

  • Ürün sayfasına öneriler bölümü ekleyin ve Hata yönetimi dersindeki SectionBoundary ile sarın. Öneriler hata verince sayfanın geri kalanı yerinde kalıyor mu?

  • Sepet sayfasına toplam fiyatı ekleyin. Hesabı sunucuda mı istemcide mi yaptınız, neden?

  • next.config.ts'e /sepet → /cart yönlendirmesini redirects ile ekleyin.

Kaynaklar

  • 1DocsNext.js: Installation

    `create-next-app` ve varsayılan ayarlar.

  • 2DocsNext.js: Caching

    `"use cache"`, `cacheLife`, `connection()`, runtime veriyi `<Suspense>` içinde okumak ve statik kabuk.

  • 3DocsNext.js: revalidateTag

    Etiketle yenileme, `"max"` profili ve stale-while-revalidate davranışı.

  • 4DocsNext.js: cookies

    Cookie'nin yalnızca Server Function ya da Route Handler'da yazılması ve aynı turda güncellenen arayüz.

  • 5DocsNext.js: Form Component

    Aramayı URL'ye yazan form ve istemci tarafı geçiş.

  • 6DocsNext.js: Adopting Partial Prefetching

    URL verisini `<Suspense>` içine taşımak ve `prefetch={true}` ile cache'li içeriği önceden getirmek.

  • 7DocsNext.js: generateStaticParams

    Dinamik segmentleri build'de prerender etmek.

  • 8DocsNext.js: Metadata and OG images

    `generateMetadata` ve sayfanın aynı veriyi tekrar okumaması.

  • 9DocsNext.js: Server Actions and Mutations

    Her action'ı güvenilmez giriş noktası saymak, cookie yazınca sayfanın yeniden render edilmesi ve tek turda veri ile arayüz.

  • 10DocsNext.js: Forms

    `bind` ile action'a argüman geçmek ve JavaScript yüklenmeden çalışan formlar.

  • 11DocsNext.js: Streaming, Status codes

    Akış başladıktan sonra `notFound()` ve `noindex`.

  • 12DocsNext.js: opengraph-image

    Kodla üretilen paylaşım görseli ve otomatik `og:image` etiketi.

  • 13DocsPlaywright: Installation

    Kurulum, tarayıcıları indirmek ve testleri çalıştırmak.

  • 14DocsNext.js: Instant Navigation

    `instant()` testi, hedef URL'yi beklemek ve `next dev`'de test API'si.

Edit this page

Last updated: October 21, 2026

Previous step11. AI agent'larla Next.js