Nuxt nedir, ne zaman seçilir?

Nuxt, Vue üzerine kurulu, açık kaynaklı bir web uygulaması çatısıdır. Vue size bileşen ve reaktiflik modelini verir; Nuxt bunun etrafına dosya tabanlı yönlendirme, sunucu tarafında render (SSR), veri çekme kalıpları, otomatik içe aktarmalar ve Nitro adlı sunucu motorunu ekler. Böylece aynı projede hem kullanıcının gördüğü sayfaları hem de bu sayfaların konuştuğu küçük API uçlarını yazabilirsiniz. Bu yazıyı hazırladığım 6 Ekim 2026 itibarıyla güncel kararlı sürüm 5 Ağustos 2026'da yayımlanan Nuxt 4.5.2; altında Nitro 2.13.4 çalışıyor. Nuxt 3'ün desteği 31 Temmuz 2026'da sona erdi, bu yüzden yeni bir projeye Nuxt 4 ile başlamak gerekiyor.

Nuxt'ı seçmek için en güçlü gerekçe, ilk yanıtın anlamlı HTML olarak gelmesinin önemli olduğu projelerdir: içerik siteleri, kataloglar, blog ve dokümantasyon sayfaları, arama motorunda bulunması gereken ürün ekranları. Sunucu ile istemci arasında aynı tipleri ve doğrulama kurallarını paylaşmak, tek bir kod tabanında hem arayüzü hem API'yi yönetmek de Nuxt'ı cazip kılar. Buna karşılık yalnızca giriş yapmış kullanıcıların gördüğü küçük bir yönetim paneli, başka bir sayfaya gömülen bir widget ya da SEO beklentisi olmayan tek ekranlık bir araç için Nuxt'ın getirdiği çalışma zamanı ve yapılandırma yükü gereksiz olabilir. Doğru soru “en iyi çatı hangisi” değil, “bu projenin ilk yanıtı nerede, ne zaman üretilmeli” sorusudur.

Bu rehber boyunca tek bir örnek projeyi büyüteceğiz: Anadolu Ürün Rehberi. Yerel üreticilerin el yapımı ürünlerini listeleyen, arama yapılabilen, her ürünün ayrı bir detay sayfası olan ve Türkçe ile İngilizce yayın yapan küçük bir katalog. Başlangıçta ürünleri bellekte tutacağız; bunun bilinçli bir sadeleştirme olduğunu, gerçek stok ve fiyat bilgisinin bir veritabanında ya da güvenilir bir servis arkasında durması gerektiğini baştan söyleyeyim. Nuxt varsayılan olarak SSR açık gelir; aşağıdaki ayar bu davranışı yalnızca görünür kılar.

nuxt.config.ts: SSR varsayılan olarak açıktır; açıkça yazmak niyeti belgeler.ts
01export default defineNuxtConfig({02  ssr: true,03})

Kurulum ve Nuxt 4 proje yapısı

Nuxt 4'ün kurulum belgesi Node.js 22 veya daha yeni bir sürüm istiyor ve 22 ya da 24 gibi çift numaralı, tercihen aktif LTS sürümünü öneriyor. Önce node -v ile sürümünüzü kontrol edin; ardından resmî başlatıcı ile projeyi oluşturup geliştirme sunucusunu açın. -o bayrağı tarayıcıyı otomatik açar.

Yeni bir Nuxt 4 projesi oluşturmak ve geliştirme sunucusunu başlatmak.sh
01npm create nuxt@latest anadolu-urun-rehberi02cd anadolu-urun-rehberi03npm run dev -- -o

Nuxt 4'ün en görünür değişikliği klasör düzenidir. Vue uygulamasına ait her şey artık varsayılan olarak app/ klasöründe durur: sayfalar, bileşenler, layout'lar, composable'lar, middleware ve eklentiler. Sunucu kodu ise kök dizindeki server/ klasöründe kalır. Kökteki shared/ klasörü hem Vue uygulamasının hem de Nitro sunucusunun kullanabileceği kodlar içindir; örneğin iki tarafın da kullandığı ürün tipi veya bir fiyat biçimlendirme fonksiyonu. public/ statik dosyaları olduğu gibi sunar. Nuxt 3 düzenindeki projeler otomatik algılandığı için taşıma zorunlu değildir; ama yeni bir projede bu ayrımı korumak, hangi kodun tarayıcıya gidebileceğini ve hangisinin yalnızca sunucuda kalması gerektiğini ilk bakışta gösterir.

Nuxt 4 dizin ağacı; app/ Vue uygulamasını, kök server/ Nitro'yu, shared/ ikisini de kapsar.
Nuxt 4'te app/ arayüz kaynaklarını, server/ Nitro kodunu, shared/ ortak yardımcıları tutar.

Bu ayrımı küçük bir güvenlik sınırı gibi düşünün. server/ altındaki bir dosya veritabanı parolası okuyabilir; app/ altındaki bir bileşen ise tarayıcıya gönderilecek pakete girer. Gizli bir anahtarı yanlışlıkla bir bileşene taşımak, onu herkesin indirebileceği JavaScript dosyasına koymak anlamına gelir. shared/ içine yalnızca iki tarafta da güvenle çalışabilecek, gizli bilgi taşımayan kodları koyun.

Sayfalar, dosya tabanlı yönlendirme ve layout

Nuxt'ta yönlendirme tablosu yazmazsınız; app/pages/ altındaki dosya adları adres olur. pages/index.vue ana sayfayı, pages/products/index.vue ürün listesini, pages/products/[slug].vue ise köşeli parantezdeki parametreyle her ürünün detay sayfasını oluşturur. Sayfalar arası geçişte NuxtLink kullanın: hem doğru <a> etiketi üretir hem de bağlantı görünür olduğunda hedef sayfanın kodunu önceden yükleyebilir. Uygulamanın kökü olan app.vue dosyası, sayfayı ve onu saran layout'u nereye çizeceğini söyler.

app/app.vue: layout ve aktif sayfa için iki yer tutucu.vue
01<template>02  <NuxtLayout>03    <NuxtPage />04  </NuxtLayout>05</template>

Layout'lar, birden çok sayfanın paylaştığı çerçevedir: üst menü, alt bilgi, ortak boşluklar. app/layouts/default.vue dosyası varsayılan çerçevedir ve sayfanın içeriği <slot /> yerine yerleşir. Bir sayfanın farklı bir layout kullanması gerekiyorsa bunu definePageMeta ile sayfanın kendi içinde belirtirsiniz; böylece yönetim paneli gibi bölümler kendi sade çerçevesini kullanabilir.

app/layouts/default.vue: tüm sayfaların paylaştığı çerçeve.vue
01<template>02  <div>03    <header><NuxtLink to="/">Anadolu Ürün Rehberi</NuxtLink></header>04    <main><slot /></main>05  </div>06</template>
app/pages/products/[slug].vue: adres parametresini okuyan detay sayfası.vue
01<script setup lang="ts">02const route = useRoute()03 04definePageMeta({05  layout: 'default',06})07</script>08 09<template>10  <article>11    <h1>Ürün detayı</h1>12    <p>Slug: {{ route.params.slug }}</p>13  </article>14</template>

Burada küçük ama önemli bir ayrıntı var: route.params.slug kullanıcıdan gelen bir değerdir. Sayfada göstermek sorun değildir, çünkü Vue şablonları metni otomatik kaçışlar. Ama bu değeri bir API adresine eklerken encodeURIComponent ile kodlayacağız ve sunucuda mutlaka doğrulayacağız. Yönlendirme kolaylığı, girdinin güvenilir olduğu anlamına gelmez.

Veri çekme: useFetch, useAsyncData ve $fetch

Nuxt'ın en çok yanlış anlaşılan, ama en değerli parçası veri çekme katmanıdır. Bir sayfa sunucuda render edilirken useFetch veya useAsyncData ile çektiğiniz veri, HTML ile birlikte “payload” adı verilen bir paket içinde tarayıcıya gönderilir. Tarayıcı sayfayı hidrasyonla canlandırırken aynı anahtara sahip çağrı bu paketi okur ve ilk isteği bir daha yapmaz. Bileşenin setup kısmında doğrudan $fetch kullanırsanız bu devir teslim olmaz: aynı istek bir kez sunucuda, bir kez tarayıcıda çalışabilir. Kural basit: sayfa açılırken gereken veri için composable'ları, kullanıcı bir düğmeye bastığında yapılan işlemler için $fetch'i kullanın.

useFetch veya useAsyncData sonucunun key ile payload'a yazılıp hydration'da yeniden kullanılma akışı.
İlk SSR verisi tekrarlanmaz; yenileme veya kullanıcı eylemi yeni istek başlatır.
app/pages/products/index.vue: SSR'da çekilen katalog ve tıklamayla gönderilen talep.vue
01<script setup lang="ts">02interface Product {03  id: number04  slug: string05  name: string06  price: number07}08 09const { data: products, status, error, refresh } = await useFetch<Product[]>('/api/products', {10  key: 'catalog:products',11  pick: ['id', 'slug', 'name', 'price'],12  default: () => [],13})14 15async function sendInquiry(productSlug: string) {16  await $fetch('/api/inquiries', {17    method: 'POST',18    body: { productSlug },19  })20}21</script>22 23<template>24  <section>25    <p v-if="status === 'pending'">Ürünler yükleniyor…</p>26    <p v-else-if="error" role="alert">Ürünler alınamadı.</p>27    <ul v-else>28      <li v-for="product in products" :key="product.id">29        <NuxtLink :to="`/products/${product.slug}`">{{ product.name }}</NuxtLink>30        <span>{{ product.price }} ₺</span>31        <button @click="sendInquiry(product.slug)">Bilgi iste</button>32      </li>33    </ul>34    <button @click="refresh()">Yenile</button>35  </section>36</template>

Bu örnekte birkaç bilinçli tercih var. Açık bir key verdik, çünkü Nuxt 4'te aynı anahtarı kullanan çağrılar aynı data, error ve status değerlerini paylaşır; bu yüzden aynı anahtarlı çağrılarda pick, transform ve default gibi seçenekleri de tutarlı tutmak gerekir. pick yalnızca listede gereken alanları payload'a koyar ve HTML'e gömülen veriyi küçültür; ancak sunucudan alınan yanıtın kendisini küçültmez. Nuxt 4'te data ve error başlangıçta undefined gelir ve veri varsayılan olarak yüzeysel reaktiftir, bu yüzden default ile boş bir dizi vermek şablonu sadeleştirir. Şablonda status ile yükleniyor, error ile hata durumunu ayrı ayrı gösteriyoruz.

Bir CMS istemcisi veya kendi yazdığınız bir async fonksiyon kullanıyorsanız useAsyncData daha uygundur. Detay sayfasında anahtarın ürüne özel olması kritiktir: sabit bir anahtar, önceden render edilen sayfalarda bir ürünün verisinin başka bir ürünün sayfasına sızmasına yol açabilir. Anahtarı bir fonksiyonla, adres parametresine bağlı üretin.

Ürüne özel anahtarla useAsyncData; parametre adrese eklenmeden önce kodlanır.ts
01const route = useRoute()02const slug = computed(() => String(route.params.slug))03const { data: product, error } = await useAsyncData(04  () => `catalog:product:${slug.value}`,05  () => $fetch(`/api/products/${encodeURIComponent(slug.value)}`),06)

Kritik olmayan, sayfanın görünmesini beklemesi gerekmeyen veriler için ise isteği tarayıcıya erteleyebilirsiniz. server: false seçeneği isteğin hidrasyon tamamlandıktan sonra başlaması demektir; bu süre boyunca status değerine göre bir yükleniyor durumu göstermek kullanıcıya dürüst bir geri bildirim verir.

Yorumlar gibi ikincil veriler için tarayıcıda gecikmeli istek.ts
01const { data: reviews, status } = useLazyFetch('/api/reviews', {02  server: false,03  default: () => [],04})

Nitro ile API ve çalışma zamanı doğrulaması

Nuxt'ın sunucu tarafını Nitro yürütür. server/api/ altına koyduğunuz her dosya bir API ucu olur ve dosya adındaki .get, .post gibi son ekler hangi HTTP yöntemine yanıt verdiğini belirler. Örneğimizde ürün verisini server/utils/products.ts içinde tutuyor, listeyi ve detayı iki ayrı uçtan sunuyoruz. #server takma adı Nuxt 4.3 ile geldi ve yalnızca sunucu kodunda kullanılabilir; bu da sunucuya ait bir modülün yanlışlıkla tarayıcı paketine girmesini zorlaştırır.

server/utils/products.ts: öğretici amaçlı bellek içi örnek veri.ts
01export const products = [02  { id: 1, slug: 'seramik-kupa', name: 'Seramik kupa', price: 420, description: 'El yapımı seramik kupa.' },03  { id: 2, slug: 'dokuma-canta', name: 'Dokuma çanta', price: 890, description: 'Yerel dokumadan çanta.' },04]

TypeScript tipleri derleme sırasında silinir; internetten gelen bir sorgu parametresinin tipini kanıtlamaz. Bu yüzden arama ucunda gelen q parametresini zod ile çalışma zamanında doğruluyor, geçersiz girdide anlaşılır bir 400 yanıtı dönüyoruz. Aramada toLocaleLowerCase('tr') kullanmak Türkçe için önemlidir: büyük “I” harfinin “ı”, büyük “İ” harfinin “i” olarak küçülmesini sağlar ve “Iğdır” araması doğru sonuç verir.

server/api/products.get.ts: doğrulanmış arama parametresiyle ürün listesi.ts
01import { z } from 'zod'02import { products } from '#server/utils/products'03 04const querySchema = z.object({05  q: z.string().trim().max(80).optional(),06})07 08export default defineEventHandler((event) => {09  const parsed = querySchema.safeParse(getQuery(event))10  if (!parsed.success) {11    throw createError({ statusCode: 400, statusMessage: 'Geçersiz arama parametresi' })12  }13 14  const q = parsed.data.q?.toLocaleLowerCase('tr')15  return q16    ? products.filter(product => product.name.toLocaleLowerCase('tr').includes(q))17    : products18})
server/api/products/[slug].get.ts: bulunamayan ürün için 404.ts
01import { products } from '#server/utils/products'02 03export default defineEventHandler((event) => {04  const slug = getRouterParam(event, 'slug')05  const product = products.find(item => item.slug === slug)06  if (!product) {07    throw createError({ statusCode: 404, statusMessage: 'Ürün bulunamadı' })08  }09  return product10})

Kullanıcının “Bilgi iste” düğmesine bastığında çalışan uç ise gövdeyi doğruluyor. Bu örnek talebi hiçbir yere kaydetmiyor; gerçek bir projede yanıt vermeden önce talebi veritabanına yazmanız ya da bir kuyruğa göndermeniz gerekir. Gizli anahtarları ise koda değil, runtimeConfig üzerinden ortam değişkenlerine koyun ve hiçbir zaman runtimeConfig.public altına yerleştirmeyin; o bölüm tarayıcıya gönderilir.

server/api/inquiries.post.ts: gövde doğrulaması yapan talep ucu.ts
01import { z } from 'zod'02 03const inquirySchema = z.object({04  productSlug: z.string().min(1).max(120),05})06 07export default defineEventHandler(async (event) => {08  const parsed = inquirySchema.safeParse(await readBody(event))09  if (!parsed.success) {10    throw createError({ statusCode: 400, statusMessage: 'Geçersiz talep' })11  }12 13  return { received: true, productSlug: parsed.data.productSlug }14})

Render modları ve routeRules ile hibrit yapı

Nuxt'ın asıl gücü, her sayfanın nasıl üretileceğine ayrı ayrı karar verebilmenizdir. SSR'da HTML her istekte sunucuda üretilir: veri tazedir ama her istek sunucuda iş demektir. SSG ya da prerender'da sayfa derleme sırasında üretilir: barındırmak çok kolaydır ama içerik değiştiğinde yeniden derleme gerekir. SPA modunda (ssr: false) tarayıcıya boş bir uygulama kabuğu gider ve arayüz tarayıcıda çizilir; arama motoru açısından en zayıf seçenektir ama özel paneller için yeterlidir. Hibrit yapı ise bu modları adres bazında birleştirir.

SSR, ön üretim, SPA ve hibrit Nuxt render modlarının beş ölçütte nitel karşılaştırması.
Render modu; ilk HTML, sunucu ihtiyacı, güncellik, barındırma ve SEO gereksinimlerine göre seçilir.

Kataloğumuz için mantıklı bir karışım şu: ana sayfa nadiren değiştiği için derleme sırasında üretilsin, ürün sayfaları bir saatlik önbellekle “stale-while-revalidate” (SWR) mantığıyla sunulsun, yönetim paneli ise yalnızca tarayıcıda çalışsın. SWR'da ziyaretçi önbellekteki sayfayı hemen alır, arka planda yeni sürüm hazırlanır. Bunun bedeli, fiyat değiştikten sonra kısa bir süre eski fiyatın görünebilmesidir; ürününüz için bu kabul edilebilir mi, buna siz karar vermelisiniz. Şunu da unutmayın: ssr: false yalnızca render biçimini değiştirir, yönetim paneli adresini veya API'sini korumaz; kimlik doğrulama ve yetkilendirme her zaman sunucuda yapılmalıdır.

nuxt.config.ts: adres bazında render ve önbellek kuralları.ts
01export default defineNuxtConfig({02  routeRules: {03    '/': { prerender: true },04    '/products/**': { swr: 3600 },05    '/admin/**': { ssr: false },06  },07})

İki uyarıyı aklınızda tutun. Birincisi, kişiye özel yanıtları (sepet, hesap bilgisi) asla paylaşılan önbelleğe koymayın; SWR herkese aynı sayfayı verir. İkincisi, bu hibrit kurallar nuxt generate ile oluşturulan tamamen statik çıktıda çalışmaz; çalışan bir Nitro sunucusu ister. isr kuralı da her platformda aynı davranmaz: Nuxt belgeleri CDN önbelleğiyle ISR desteği için Vercel ve Netlify'ı gösteriyor. Tamamen statik bir site istiyorsanız dinamik ürün adreslerinin derleme sırasında keşfedildiğinden ya da listelendiğinden emin olun.

Render modu bir teknoloji tercihi değil, ürün kararıdır: her adres için “ne kadar taze olmalı, ne kadar ucuza sunulmalı?” sorusunu cevaplayın.

SEO ve Türkçe/İngilizce içerik

SSR, arama motorlarına anlamlı HTML verir ama başlık, açıklama ve dil etiketlerini sizin doğru kurmanız gerekir. Her ürün sayfası kendi başlığını ve açıklamasını yüklenen veriden almalı. useSeoMeta reaktif değerleri kabul eder ve değerleri kaçışlayarak etiketlere yazar; ürün bulunamazsa bile anlamlı bir varsayılan gösterin.

Ürün detay sayfasında veriden üretilen başlık ve açıklama.ts
01const route = useRoute()02const { data: product } = await useFetch<{ name: string; description: string }>(03  () => `/api/products/${encodeURIComponent(String(route.params.slug))}`,04)05const title = computed(() => product.value?.name ?? 'Ürün bulunamadı')06const description = computed(() => product.value?.description ?? 'Anadolu Ürün Rehberi')07useSeoMeta({ title, description, ogTitle: title, ogDescription: description })

İki dilli yayın için resmî i18n modülünü ve site haritası modülünü ekleyin. prefix_except_default stratejisi Türkçe adresleri önek olmadan, İngilizce adresleri /en/ önekiyle üretir. language alanına tr-TR ve en-US gibi tam dil etiketleri yazmak, baseUrl ile de üretim alan adını vermek önemlidir; aksi hâlde hreflang ve canonical bağlantıları mutlak adres olarak üretilemez.

i18n ve sitemap modüllerini eklemek.sh
01npx nuxi@latest module add i18n02npx nuxi@latest module add sitemap
nuxt.config.ts: Türkçe varsayılan, İngilizce /en/ önekli iki dilli yapı.ts
01export default defineNuxtConfig({02  modules: ['@nuxtjs/i18n', '@nuxtjs/sitemap'],03  i18n: {04    baseUrl: 'https://katalog.example',05    defaultLocale: 'tr',06    strategy: 'prefix_except_default',07    locales: [08      { code: 'tr', language: 'tr-TR', file: 'tr.json' },09      { code: 'en', language: 'en-US', file: 'en.json' },10    ],11  },12})
i18n/locales/tr.json: Türkçe arayüz metinleri.json
01{ "catalog": { "title": "Ürünler", "empty": "Ürün bulunamadı" } }
i18n/locales/en.json: İngilizce arayüz metinleri.json
01{ "catalog": { "title": "Products", "empty": "No products found" } }
app/app.vue: dil, hreflang ve canonical etiketlerini her sayfaya eklemek.vue
01<script setup lang="ts">02const localeHead = useLocaleHead({ seo: true })03 04useHead(() => ({05  htmlAttrs: localeHead.value.htmlAttrs ?? {},06  link: localeHead.value.link ?? [],07  meta: localeHead.value.meta ?? [],08}))09</script>10 11<template><NuxtLayout><NuxtPage /></NuxtLayout></template>

Burada önemli bir sınır var: i18n modülü adresleri, arayüz metinlerini ve SEO etiketlerini iki dile taşır, ama ürün verisini kendiliğinden çevirmez. Şimdiye kadar ürünlerimiz yalnızca Türkçeydi. Gerçekten iki dilli bir katalog için her ürünün adını ve açıklamasını iki dilde tutun ve API'nin istenen dili döndürmesini sağlayın. Dil parametresini de diğer girdiler gibi doğrulayın.

server/utils/products.ts: her ürün iki dilde.ts
01export const products = [02  {03    id: 1,04    slug: 'seramik-kupa',05    price: 420,06    name: { tr: 'Seramik kupa', en: 'Ceramic mug' },07    description: { tr: 'El yapımı seramik kupa.', en: 'Handmade ceramic mug.' },08  },09  {10    id: 2,11    slug: 'dokuma-canta',12    price: 890,13    name: { tr: 'Dokuma çanta', en: 'Woven bag' },14    description: { tr: 'Yerel dokumadan çanta.', en: 'A bag made from local weaving.' },15  },16]

Liste ucu artık hem aramayı hem dili doğruluyor; arama, istenen dildeki ürün adında yapılıyor. Detay ucu da aynı dil parametresini doğrulayıp ürünün o dildeki adını ve açıklamasını döndürüyor. Bu iki dosya, bölüm 5'teki tek dilli sürümlerin yerini alır.

server/api/products.get.ts: doğrulanmış arama ve dil parametresiyle ürün listesi.ts
01import { z } from 'zod'02import { products } from '#server/utils/products'03 04const querySchema = z.object({05  q: z.string().trim().max(80).optional(),06  locale: z.enum(['tr', 'en']).default('tr'),07})08 09export default defineEventHandler((event) => {10  const parsed = querySchema.safeParse(getQuery(event))11  if (!parsed.success) {12    throw createError({ statusCode: 400, statusMessage: 'Geçersiz arama parametresi' })13  }14 15  const { q, locale } = parsed.data16  const needle = q?.toLocaleLowerCase(locale)17  return products18    .map(product => ({19      id: product.id,20      slug: product.slug,21      price: product.price,22      name: product.name[locale],23      description: product.description[locale],24    }))25    .filter(product => !needle || product.name.toLocaleLowerCase(locale).includes(needle))26})
server/api/products/[slug].get.ts: istenen dilde ürün detayı, yoksa 404.ts
01import { z } from 'zod'02import { products } from '#server/utils/products'03 04const querySchema = z.object({05  locale: z.enum(['tr', 'en']).default('tr'),06})07 08export default defineEventHandler((event) => {09  const parsed = querySchema.safeParse(getQuery(event))10  if (!parsed.success) {11    throw createError({ statusCode: 400, statusMessage: 'Geçersiz dil' })12  }13 14  const { locale } = parsed.data15  const product = products.find(item => item.slug === getRouterParam(event, 'slug'))16  if (!product) {17    throw createError({ statusCode: 404, statusMessage: 'Ürün bulunamadı' })18  }19  return { ...product, name: product.name[locale], description: product.description[locale] }20})

Sayfalarda aktif dili sorguya ekleyin. query içindeki reaktif değer değiştiğinde useFetch isteği yeniler; böylece kullanıcı dili değiştirdiğinde liste ve detay da o dilde gelir. Detay sayfasındaki SEO etiketleri de artık seçili dildeki ürün verisinden üretilir.

app/pages/products/index.vue: aktif dili API isteğine eklemek.ts
01const { locale } = useI18n()02const { data: products } = await useFetch('/api/products', {03  query: { locale },04  default: () => [],05})
app/pages/products/[slug].vue: dile göre ürün detayı ve SEO etiketleri.ts
01const route = useRoute()02const { locale, t } = useI18n()03const { data: product } = await useFetch<{ name: string; description: string }>(04  () => `/api/products/${encodeURIComponent(String(route.params.slug))}`,05  { query: { locale } },06)07const title = computed(() => product.value?.name ?? t('catalog.empty'))08const description = computed(() => product.value?.description ?? t('catalog.title'))09useSeoMeta({ title, description, ogTitle: title, ogDescription: description })

Site haritası modülü statik sayfaları kendisi bulur, ama /products/[slug] gibi dinamik adreslerin hangi ürünlerden oluştuğunu bilemez. Ürün adreslerini bir kaynak ucuyla ona verin. _i18nTransform: true, her adresin Türkçe ve İngilizce karşılığının site haritasına alternatifleriyle girmesini sağlar.

server/api/__sitemap__/urls.ts: ürün adreslerini site haritasına veren kaynak.ts
01import { products } from '#server/utils/products'02 03export default defineSitemapEventHandler(() => {04  return products.map(product => ({05    loc: `/products/${product.slug}`,06    _i18nTransform: true,07  }))08})
nuxt.config.ts: site haritası kaynağını kaydetmek.ts
01export default defineNuxtConfig({02  sitemap: {03    sources: ['/api/__sitemap__/urls'],04  },05})

Kurulumdan sonra işin bittiğini varsaymayın, çıktıyı kontrol edin. Üretim derlemesindeki bir ürün sayfasının kaynağını açıp iki dilin birbirini hreflang ile gösterdiğini, canonical adresin doğru olduğunu ve iki dildeki ürün adreslerinin site haritasına girdiğini görün. Bu kontrolü kolaylaştırmak için sitemdeki ücretsiz hreflang oluşturucu ve sitemap doğrulayıcı araçlarını kullanabilirsiniz.

Performans ve test

Katalog sitelerinde en ağır yük genellikle görsellerdir. @nuxt/image modülünün <NuxtImg> bileşeni genişlik ve yüksekliği bildirerek sayfa kaymasını önler, sizes ile ekrana uygun boyutu seçer ve ekran dışındaki görselleri loading="lazy" ile ertelemenizi kolaylaştırır. Bileşeni kullanmadan önce modülü kurmanız gerektiğini unutmayın.

app/components/ProductCard.vue: boyutları bildirilmiş, duyarlı ürün görseli.vue
01<template>02  <article>03    <NuxtImg04      :src="`/images/${product.slug}.webp`"05      :alt="product.name"06      width="720"07      height="540"08      sizes="100vw sm:50vw lg:33vw"09      loading="lazy"10    />11    <h2>{{ product.name }}</h2>12  </article>13</template>14 15<script setup lang="ts">16defineProps<{ product: { slug: string; name: string } }>()17</script>
Pencere ışığında laptop, seramik kupa ve bitkili sakin bir yazılım geliştirici çalışma masası.
Soyut kod ekranı olan dizüstü bilgisayarla sade, doğal ışıklı bir çalışma alanı.

JavaScript tarafında da benzer bir disiplin gerekir. Sayfanın ilk görünümünde gerekmeyen bileşenleri Lazy önekiyle ve bir koşula bağlayarak yükleyin; örneğin yorum paneli ancak kullanıcı istediğinde indirilsin. Payload'a koyduğunuz veriyi pick ile sınırlayın, çünkü bu veri HTML'in içine gömülür ve sayfanın ilk yükünü büyütür.

Yorum paneli yalnızca kullanıcı açtığında yüklenir.vue
01<script setup lang="ts">02const showReviews = ref(false)03</script>04 05<template>06  <button @click="showReviews = true">Yorumları göster</button>07  <LazyReviewPanel v-if="showReviews" />08</template>

Test tarafında resmî @nuxt/test-utils paketi, uygulamanızı gerçek bir sunucu olarak başlatıp sayfa HTML'ini sınamanızı sağlar. Başlangıç şablonu test araçlarını kurmaz; önce paketleri geliştirme bağımlılığı olarak ekleyin. Tarayıcıda çalışan testler için ayrıca Nuxt test belgesindeki ek kurulumu uygulayın.

Test bağımlılıklarını kurmak ve testleri çalıştırmak.sh
01npm i -D @nuxt/test-utils vitest02npx vitest run

Bu kurulumla çalışan aşağıdaki test, ana sayfanın sunucuda gerçekten render edildiğini doğrular. Bunu tarayıcıda gezinme, dil bağlantıları, yükleniyor ve hata durumları ile klavye erişimini sınayan uçtan uca testlerle tamamlayın; tip denetimi tek başına test değildir.

test/e2e/products.test.ts: sunucu render'ını doğrulayan uçtan uca test.ts
01import { describe, expect, it } from 'vitest'02import { fileURLToPath } from 'node:url'03import { $fetch, setup } from '@nuxt/test-utils/e2e'04 05describe('product catalog', async () => {06  await setup({07    rootDir: fileURLToPath(new URL('../..', import.meta.url)),08    server: true,09  })10 11  it('renders product content on the server', async () => {12    const html = await $fetch('/')13    expect(html).toContain('Anadolu Ürün Rehberi')14  })15})

Yayına alma ve sık yapılan hatalar

Nuxt'ı yayına almanın iki temel yolu var. Hibrit kurallar ve API uçları kullanıyorsanız nuxt build ile bir Node sunucusu üretir ve .output/server/index.mjs dosyasını çalıştırırsınız. Hiçbir sunucu kodu çalıştırmayan, tamamen statik bir site için nuxt generate yeterlidir; ama o durumda server/api uçlarınız çalışmaz. Vercel, Netlify veya Cloudflare gibi platformlar için Nitro'nun hazır ayar setleri (preset) vardır; platforma özel derleme ve çalışma zamanı gereksinimlerini seçmeden önce Nitro'nun yayın belgesinden kontrol edin.

Node sunucusu ve statik çıktı için derleme komutları.sh
01# Node sunucusu / Node server02npx nuxt build03NODE_ENV=production node .output/server/index.mjs04 05# Tamamen statik çıktı / Fully static output06npx nuxt generate
nuxt.config.ts: hedef platform için Nitro preset'ini açıkça seçmek.ts
01export default defineNuxtConfig({02  nitro: {03    preset: 'node-server',04  },05})

Yayından önce aşağıdaki listeyi tek tek kontrol edin. Bu maddelerin çoğu, Nuxt projelerinde canlıya çıktıktan sonra fark edilen ve düzeltmesi daha pahalı olan hatalardır:

  • Sayfa açılırken gereken veri için setup içinde doğrudan $fetch kullanmayın; useFetch veya useAsyncData kullanın.
  • Dinamik sayfalarda veri anahtarını adres parametresine bağlayın; sabit anahtar veri karışmasına yol açar.
  • TypeScript tiplerinin gelen veriyi doğruladığını varsaymayın; sunucuda çalışma zamanı doğrulaması yapın.
  • Gizli anahtarları runtimeConfig.public altına koymayın; o bölüm tarayıcıya gider.
  • Kişiye özel yanıtları SWR veya paylaşılan önbelleğe almayın.
  • nuxt generate ile çalışan bir Nitro sunucusu beklemeyin; dinamik adreslerin prerender edildiğini doğrulayın.
  • Payload boyutunu ve görselleri üretim derlemesinde ölçün; 404 sayfasını ve geçersiz API girdisini test edin.
  • Hedef platformun preset'iyle bir üretim derlemesini yayından önce mutlaka çalıştırın.

Nuxt; doğru kullanıldığında içerik sitelerinden ürün kataloglarına kadar pek çok projede hem hızlı bir ilk yanıt hem de bakımı kolay tek bir kod tabanı sunar. Asıl beceri, çatının sunduğu her özelliği kullanmak değil, her sayfanın nerede ve ne zaman üretileceğini bilerek seçmektir. Bu rehberdeki kataloğu kendi projenize uyarlarken her adımı üretim derlemesinde doğrulayın; geliştirme sunucusunda çalışan her şey canlıda aynı davranmayabilir.

Resmî kaynaklar ve ileri okuma

Bu yazıdaki sürüm bilgileri ve API davranışları aşağıdaki birincil kaynaklardan 6 Ekim 2026'da kontrol edildi:

  1. Nuxt 4: Introduction
  2. Nuxt 4: Installation
  3. Nuxt 4: Directory Structure
  4. Nuxt 4: Upgrade Guide
  5. Nuxt Blog: Nuxt 4.5
  6. Nuxt: GitHub releases
  7. Nuxt 4: Data Fetching
  8. Nuxt 4: useFetch
  9. Nuxt 4: useAsyncData
  10. Nuxt 4: server/ directory
  11. Nuxt 4: Rendering Modes
  12. Nuxt 4: Prerendering
  13. Nuxt 4: SEO and Meta
  14. Nuxt i18n: SEO
  15. Nuxt Image: NuxtImg
  16. Nuxt 4: Testing
  17. Nuxt 4: Deployment
  18. Nitro: Deploy
  19. Nuxt Design Kit

Nuxt hızlı gelişen bir çatı. Sürüme bağlı ayrıntılarda kendi projenizin kullandığı Nuxt sürümünün belgelerini esas alın. Nuxt logosu Vercel'in tescilli markasıdır; bu yazıda yalnızca Nuxt'ı tanıtmak için kullanılmıştır.

✳

İyi bir Nuxt uygulaması, her sayfanın nerede ve ne zaman üretildiğini bilerek kurulur.

Diğer yazılara göz at ↗